Food Truck 是一个基于 Javascript(Typescript) 开发的餐车点餐系统, 该系统包括了用户短信注册, 点餐, 下单支付*的完整流程.
将开发过程分为7个步骤, 每个步骤在 Github 的代码仓库中都有对应的分支(从 *phase/1 到 phase/7), 方便参考.
Food Truck is an online food ordering app based on Javascript(Typescript), this system covers the flow from registering, menu navigating and placing orders. I split this flow into 7 steps, each step corresponds to a branch of this repository.
- Day 1 搭建前后端项目脚手架 Building scaffolding for the back-end and the front-end projects
- Day 2 重构后端项目, 前端引入状态管理 refactoring back-end project, and introducing state management into the front-end project
- Day 3 前端的 UI 设计和开发 Designing and developing UI at the front-end
- Day 4 手机短信注册和登录 Signin / signup with OTP
- Day 5 数据库的支持 Database supporting
- Day 6 支付 Making a payment with Stripe
- Day 7 持续集成 Continuous integration and continuous deployment (CI/CD)
- 编外 Others
虽然说7天从头到尾开发一个全栈的应用有些夸张, 但是本文展示了如何运用成熟的云原生技术, 高效的搭建一个应用可能性. 这是一个针对有一定开发经验的初级教程, 所以并不会深入讨论每一个技术点, 但是会较全面的覆盖开发一个全栈应用的所有方面, 也包括持续集成(CI/CD) 方面的内容.
Although it is an exaggeration to develop a full-stack application in 7 days, but this article shows how to use mature cloud technology to build an application possibility. This is a primary tutorial for new bees with a little development experience, so it will not deep dive every technical point, but it will cover all aspects of developing a full-stack application, including continuous integration (CI/ CD).
服务端采用了 Node.js 和 Koa2 框架. 用户鉴权是基于 AWS 的 Cognito, 注册登入涉及的后台 Lambda Function 是在前端的工程里, 由 AWS Amplify 提供支持. 支付采用了 Stripe 提供的服务.
The server uses the Node.js and Koa2 frameworks. User authentication is based on Cognito of AWS, the backend Lambda Function involved in registration and login is in the front-end project and is supported by AWS Amplify. Payment uses the service provided by Stripe.
前端采用 Vue3, 考虑第三方库的兼容性问题, 没有采用 Vite 作为开发和打包工具.
The front-end uses Vue3
作为抛砖引玉, 这篇文章提供一个大家可以学习交流的起点, 有兴趣的同学欢迎加微信和笔者互相交流.
As an introduction, this article provides a starting point for everyone to learn and communicate. It is welcome to add WeChat to communicate with the author.
相关的代码仓库如下:
The relevant code repositories are as follows:
代码仓库暂时不公开, 有需要交流学习的同学可以添加联系方式。
Day 1 搭建前后端项目脚手架
”工欲善其事,必先利其器“
If a worker wants to do his job well, he must first sharpen his tools
- 高效的开发环境和顺畅的持续集成流程决定了开发的效率. 在 MacOS 下建议安装
iTerm2,Oh-My-Zsh并配置Powerlevel10K的 theme. 这里不详细展开这些工具的安装了, 有兴趣的同学可以上网搜一下. 这是我的开发环境.
An efficient development environment and a smooth continuous integration process guarantees the efficiency of development. Under MacOS, it is recommended to install iTerm2, Oh-My-Zsh and then configure the theme of Powerlevel10K. The installation of these tools will not be discussed here, if you are interested in these tools, you can google it. This is my development environment.
- 如果是一个团队参与一个项目的开发, 在开发时统一代码规范也是很重要的, 这个章节会重点描述
ESLint和Prettier的配置来规范团队的代码.
It is very important to unify the code style during development if a team is involved in a project. This section will focus on the configuration of ESLint and Prettier to standardize the team’s code style.
从零到一创建一个后端的工程(create backend project from scratch)
- 先在
Github上创建一个仓库ft-node
createft-noderepository onGithubfirst
- 初始化本地
git仓库, 并推送到Github仓库中
initialize the local repository, and push it onto the remote repository underGithub
1 | shell复制代码mkdir backend && cd backend |
- 添加
.gitignore
add.gitignorefile
1 | bash复制代码# .gitignore |
- 初始化
npm和Typescript项目
Intialize the node.js project withnpm, and configureTypescript
1 | shell复制代码# 创建代码目录 |
- 安装
Koa,Koa Router和@Koa/cors(支持跨域), 以及对应的类型依赖
1 | shell复制代码npm i koa koa-router @koa/cors |
- 创建一个简单
Koa服务
- 在 src 目录下创建 server.ts 文件
1 | typescript复制代码// server.ts |
- 测试
1 | shell复制代码npx tsc |
- 安装
ts-node和nodemon, Running the server with Nodemon and TS-Node, 并修改package.json中的脚本
1 | shell复制代码npm i ts-node nodemon --save-dev |
修改 package.json
1 | json复制代码 "scripts": { |
- 配置
ESLint和Prettier
- 安装
eslint和相关依赖
1 | shell复制代码npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev |
- eslint: The core ESLint linting library
- @typescript-eslint/parser: The parser that will allow ESLint to lint TypeScript code
- @typescript-eslint/eslint-plugin: A plugin that contains a bunch of ESLint rules that are TypeScript specific
- 添加 .eslintrc.js 配置文件
可以通过, 以交互的方式创建
1 | shell复制代码npx eslint --init |
但是建议手动在项目的 root 目录下创建这个文件
1 | javascript复制代码// .eslintrc.js |
- 添加
Prettier
1 | shell复制代码npm i prettier eslint-config-prettier eslint-plugin-prettier --save-dev |
- prettier: The core prettier library
- eslint-config-prettier: Disables ESLint rules that might conflict with prettier
- eslint-plugin-prettier: Runs prettier as an ESLint rule
In order to configure prettier, a .prettierrc.js file is required at the root project directory. Here is a sample .prettierrc.js file:
1 | javascript复制代码// .prettierrc.js |
Next, the .eslintrc.js file needs to be updated:
1 | javascript复制代码// eslintrc.js |
- Automatically Fix Code in VS Code
- 安装
eslint扩展, 这里不需要安装prettier扩展
并点击右下角激活 eslint 扩展 - 在 VSCode 中创建 Workspace 的配置
- 安装
1 | json复制代码{ |
- Run ESLint with the CLI
- A useful command to add to the
package.jsonscripts is a lint command that will run ESLint.
- A useful command to add to the
1 | json复制代码{ |
- 添加
.eslintignore
1 | bash复制代码# don't ever lint node_modules |
- Preventing ESLint and formatting errors from being committed
1 | shell复制代码npm install husky lint-staged --save-dev |
To ensure all files committed to git don’t have any linting or formatting errors, there is a tool called lint-staged that can be used. lint-staged allows to run linting commands on files that are staged to be committed. When lint-staged is used in combination with husky, the linting commands specified with lint-staged can be executed to staged files on pre-commit (if unfamiliar with git hooks, read about them here).
To configure lint-staged and husky, add the following configuration to the package.json file:
1 | json复制代码{ |
初始化 Vue3 前端
- 通过 Vue Cli 创建前端项目
1 | shell复制代码# 参看 vue cli 的版本 |
- 用
Vue Cli创建的脚手架已经配置好大部分ESLint和Prettier, 这里只要统一一下前后端的Prettier设置
- 添加 .prettierrc.js 文件
1 | javascript复制代码// .prettierrc.js |
- 同样修改 workspace 的配置
1 | json复制代码{ |
- 运行 npm run lint
前端引入 Axois, 访问接口(health interface)
- 安装 axios 模块
1 | shell复制代码npm i axios --save |
- 封装 axios 模块, 设置默认 URL
1 | typescript复制代码// src/utils/axios.ts |
- 封装 src/apis/health.ts 文件
1 | typescript复制代码// src/apis/health.ts |
- 在 App.vue 中调用 checkHealth 接口
1 | vue复制代码<script lang="ts"> |
这个阶段完成了一个最简单 APP 的前后端工程的搭建. 我这里规范了前后端的代码, 同学可以根据自己团队的代码规范添加不同的规则.
Javascript & Typescript Essential
Day 2 重构后端项目 前端引入状态管理
重构服务端
引入服务端日志库 logger 和 winston
1 | shell复制代码# logger 作为记录请求的中间件 |
效果如下
引入 dotenv , 从环境变量和环境变量配置文件读取配置
- node 的后端服务是在运行时动态加载这些环境变量的,代码里我采用了 dotenv 模块来加载环境变量
1 | shell复制代码npm install dotenv --save |
- 读取环境变量
1 | typescript复制代码// src/config/index.ts |
- ** 两个注意点 **
- 如果
NODE_ENV设置为production,npm ci不会安装devDependencies中的依赖包,如果在运行的EC2上编译打包,编译会报错。所以打包编译我放在了Github Actions的容器中了,所以避免了这个问题
We have installed all our packages using the –save-dev flag. In production, if we use npm install –production these packages will be skipped.
- 操作系统下设置的环境变量会覆盖 .env 文件中定义的环境变量, 一般会把敏感的信息, 例如密码和密钥直接定义在系统的环境变量里, 而不是放在
.env的文件下, 这样避免将代码提交到公开的代码仓库带来安全的隐患.
- 配置文件的加载逻辑都放到
config目录下
服务端拆解 App 和 Server
index.ts只保留了启动http服务和其他连接数据库的服务
1 | typescript复制代码// src/index.ts |
app.ts负责各种中间件的加载, 并安装bodyParser
1 | shell复制代码npm install koa-bodyparser --save |
1 | typescript复制代码// src/app.ts |
搭建 RCS 模型, 将 server.ts 拆成 http 服务, Koa2 中间件和 koa-router 三个模块, 符合单一职责的设计原理
the logic should be divided into these directories and files.
- Models - The schema definition of the Model
- Routes - The API routes maps to the Controllers
- Controllers - The controllers handles all the logic behind validating request parameters, query, Sending Responses with correct codes.
- Services - The services contains the database queries and returning objects or throwing errors
router拆解到apis目录下, 按模块提供不同的接口, 并通过动态的方式加载
1 | typescript复制代码// src/apis/index.ts |
- apis 下各个子模块的拆解如下图
构建前端路由, 引入 Vue 3.0 的状态管理 Provide 和 Inject
用 Vue3 的 Provide 和 Inject 替代 Vuex
参考 Using provide/inject in Vue.js 3 with the Composition API
用 npm 搭建共享的 Lib, 共享数据对象(Building and publishing an npm typescript package)
参考 Step by step: Building and publishing an NPM Typescript package.
- 创建目录
1 | shell复制代码mkdir lib && cd lib |
- 初始化
git仓库
1 | shell复制代码git init |
- 初始化
npm项目
1 | shell复制代码npm init -y |
- 配置
tsconfig.json
1 | json复制代码{ |
- ignore
/lib
1 | shell复制代码echo "/lib" >> .gitignore |
- 配置
eslint
- 安装 eslint 和 typescript 相关依赖
1 | shell复制代码npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev |
- 创建
.eslintrc.js
1 | javascript复制代码// .eslintrc.js |
- 安装
prettier
1 | shell复制代码npm i prettier eslint-config-prettier eslint-plugin-prettier -D |
- 创建
.prettierrc.js
1 | javascript复制代码// .prettierrc.js |
- 配置 vscode workspace
1 | json复制代码{ |
- 添加 .eslintignore
1 | bash复制代码node_modules |
- 配置 npm 库的输出文件
However, blacklisting files is not a good practice. Every new file/folder added to the root, needs to be added to the .npmignore file as well! Instead, you should whitelist the files /folders you want to publish. This can be done by adding the files property in
package.json:
1 | json复制代码 "files": [ |
- Setup Testing with Jest
- 安装
Jest
1 | shell复制代码npm install --save-dev jest ts-jest @types/jest |
- Create a new file in the root and name it jestconfig.json:
1 | json复制代码{ |
- Remove the old test script in
package.jsonand change it to:
1 | json复制代码"test": "jest --config jestconfig.json", |
- 准备一个待测试的函数
index.ts
1 | typescript复制代码// index.ts |
- Write a basic test
In the src folder, add a new folder called tests and inside, add a new file with a name you like, but it has to end with
test.ts, for examplegreeter.test.ts
1 | typescript复制代码// greeter.test.ts |
then try to run
1 | shell复制代码npm test |
- Use the magic scripts in NPM
- 修改脚本
package.json
1 | json复制代码 "prepare": "npm run build", |
- Updating your published package version number
To change the version number in package.json, on the command line, in the package root directory, run the following command, replacing <update_type> with one of the semantic versioning release types (patch, major, or minor):
1 | shell复制代码npm version <update_type> |
- Run npm publish.
- Finishing up
package.json
1 | json复制代码 "description": "share library between backend and frontend", |
- Publish you package to NPM!
- run
npm login
1 | shell复制代码╰─ npm login ─╯ |
- install
quboqin-libon both sides(frontend and backend), and add test code
1 | shell复制代码npm i quboqin-lib |
Day 3 前端的界面设计和开发
创建数据模型, 更新 quboqin-lib
前端引入 Tailwind CSS UI 库
- 用
Figma设计一个 UI 的原型 - 安装
Tailwind
这是一个基于utility-first思想的 UI 库, 在它基础上上可以方便搭建 UI 界面, 基本不用在代码里写一行css. 也方便后期维护
安装中要注意 PostCSS 7 的兼容性
- 安装
1 | shell复制代码npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 |
- Add Tailwind as a PostCSS plugin
Add tailwindcss and autoprefixer to your PostCSS configuration.
1 | javascript复制代码// postcss.config.js |
- Customize your Tailwind installation
1 | shell复制代码npx tailwindcss init |
- Include Tailwind in your CSS
1 | css复制代码/* index.css */ |
then import this file in main.ts
1 | typescript复制代码import { createApp } from 'vue' |
- When building for production, be sure to configure the purge option to remove any unused classes for the smallest file size:
1 | javascript复制代码// tailwind.config.js |
- 其他组件库
可以用Tailwind开发 UI 的组件, 这个工程里为了快速开发, 这里还引入了Vant, 这是一个较早支持 Vue 3 的 UI 库
1 | shell复制代码# Install Vant 3 for Vue 3 project |
- 用到了
materialdesignicons, 在html中添加
1 | html复制代码<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet"> |
封装 Axios 和接口, 提供 Mock 数据
1 | typescript复制代码// src/apis/goods.ts |
本地支持HTTPS[Server]
- 在项目的
root目录下创建local-ssl子目录, 在local-ssl子目录下,新建一个配置文件req.cnf
1 | ini复制代码[req] |
- 在
local-ssl目录下创建本地证书和私钥
1 | shell复制代码openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout cert.key -out cert.pem -config req.cnf -sha256 |
- 修改
server端代码支持本地https
1 | typescript复制代码// src/index.ts |
- 启动
https服务后,仍然报错
1 | shell复制代码curl https://localhost:3000/api/v1/users |
在浏览器链接该 https 服务,也报错误,下载证书,双击导入密钥管理器,手动让证书受信
- 前端接口也改成
https访问
1 | typescript复制代码// src/utils/axios.ts |
Day 4 手机登录和注册
用户鉴权和手机登录用到了 AWS 的 Cognito 和 Amplify 等服务
在前端工程里初始化 Amplify 和 Cognito
- 初始化
Amplify
1 | shell复制代码amplify init |
默认创建了 dev 后端环境
- 添加
Auth服务
1 | shell复制代码amplify add auth |
**一定要选择 Manual Configuration **, 具体参数如下
否则 Cognito 后台配置会默认为 Username 方式鉴权, 而且无法修改
- 添加
PreSignup的函数[选择Manual Configuration可以在命令行里添加, 这步可以跳过]
1 | shell复制代码amplify fucntion add |
- 更新这个四个
Lambda Functions的逻辑 - 推送
dev环境
1 | shell复制代码amplify push |
- 进入
AWS Console的Cognito, 手动绑定PreSignup函数[选择Manual Configuration在命令行里已经绑定, 这步可以跳过] - 给
CreateAuthChallenge添加发送短信权限
进入Lambda的 后台页面,找到对应环境下dev的foodtruck4d6aa6e5CreateAuthChallenge函数
找到对应的 Role,在Role的后台添加 SNS 权限 - 配置 SNS
- 前端添加
Amplify模块
- 安装依赖包
1 | shell复制代码npm i aws-amplify aws-amplify-vue --save |
- 初始化
Amplify配置
1 | typescript复制代码// main.ts |
** 在 tsconfig.json 中添加
1 | json复制代码"allowJs": true, |
添加 aws-exports.d.ts
1 | typescript复制代码// eslint-disable-next-line @typescript-eslint/ban-types |
这两步都要, 否则打包会报以下错误
1 | shell复制代码TS7016: Could not find a declaration file for module './aws-exports'. '/Users/qinqubo/magic/projects/full-stack/food-truck/frontend/src/aws-exports.js' implicitly has an 'any' type. |
- 团队其他成员如果在新的环境下开发
clone 代码仓库后, 在项目根目录下
1 | shell复制代码amplify init |
Do you want to use an existing environment 选 Yes
后端工程添加 JWT 模块
- 添加
JWT的中间件的依赖
1 | shell复制代码npm i jsonwebtoken jwk-to-pem --save |
- 添加
JWT中间件, 检查cognito的有效性
1 | typescript复制代码// src/jwt/cognito.ts |
- 修改 app.ts
1 | typescript复制代码// src/app.ts |
- apis 目录下分受保护的和不受保护的两类目录, 并修改路由加载逻辑
1 | typescript复制代码// src/apis/index.ts |
用 Postman 测试
- 设置Content-Type为application/json
- 设置Token
- Query parameters
1 | bash复制代码https://localhost:3000/api/v1/goods |
Day 5 数据库的支持
Postgres 和数据模型
- 在后端项目安装
postgres相关package
1 | shell复制代码npm i typeorm reflect-metadata pg --save |
- 改造共享库
- 在共享库安装
typeorm
1 | shell复制代码npm i typeorm --save |
之后更新前后端的依赖
- 在后端工程里安装
uuid
1 | shell复制代码npm i uuid --save |
- 通过 docker 连接数据库
- 添加 docker-compose.yml
1 | yaml复制代码version: '3.1' |
- 添加 postgres 配置和连接代码
1 | typescript复制代码// src/config/index.ts |
- 导入
goods数据
1 | shell复制代码npm run import-goods |
Jest 测试服务层和数据库
- 安装
Jest
1 | shell复制代码npm i jest @types/jest ts-jest --save-dev |
- 配置 Jest
1 | javascript复制代码// jest.config.js |
1 | javascript复制代码// ormconfig.js |
- 在 src/tests 下添加两个测试用例
axois 不直接支持 URL的 Path Variable, .get(‘/:phone’, controller.getUser) 路由无法通过 axois 直接访问, 修改 getUsers, 通过 Query Parameter 提取 phone
- Path parameters
** axois发出的请求不支持这种方式 **,可以在浏览器和Postman里测试这类接口
1 | url复制代码https://localhost:3000/api/v1/addresses/24e583d5-c0ff-4170-8131-4c40c8b1e474 |
对应的route是
1 | typescript复制代码 router |
下面的控制器里演示是如何取到参数的
1 | typescript复制代码 public static async getAddress(ctx: Context): Promise<void> { |
- 如何现在Postman里测试接口
设置Content-Type为application/json
设置Token
- Query parameters
1 | bash复制代码https://localhost:3000/api/v1/addresses?phone=%2B13233013227&id=24e583d5-c0ff-4170-8131-4c40c8b1e474 |
- Koa2 对应 Axios 返回的值, 以下是伪代码
1 | typescript复制代码// Koa ctx: Context |
response.data 对应 ctx.body, 所以在 Axios 获取 response 后, 是从 response.data.data 得到最终的结果
1 | typescript复制代码function get<T, U>(path: string, params: T): Promise<U> { |
Day 6 手机支付
服务端的实现
- 引入
stripe库
1 | shell复制代码npm i stripe --save |
- 配置
stripe
1 | typescript复制代码// src/utils/stripe.ts |
- 在订单接口中发起支付调用
前端的实现
- 在
html文件里添加stripe全局模块
1 | html复制代码 <script src="https://js.stripe.com/v3/"></script> |
- 添加
stripe的封装
1 | javascript复制代码// scr/utils/stripe.js |
- 添加开发环境配置
.env
.env.development
改造 axois 读取配置
1 | typescript复制代码// src/utils/axios.ts |
- 修改 CreditCardDetail.vue
Day 7 持续集成
前端编译环境和 Heroku, Amplify, 自建 Oracle VPS 的部署
部署到 Heroku
- 创建
staging和production两个独立的 APP
与服务端部署不一样,两个环境都是在Webpack打包时注入了环境变量,两个环境都需要走 CI 的编译打包流程,所以通过Pipeline的Promote是没有意义的。我们这里创建两个独立的 APP:[staging-ft, production-ft] - 为每个 APP 添加编辑插件, 因为是静态部署,比
Node Server多了一个插件
1 | shell复制代码heroku buildpacks:add heroku/nodejs |
并且要添加 static.json 文件在项目的根目录下
1 | json复制代码{ |
- 在两个 APP 的设置中绑定关联的 Github 仓库和分支
这里的VUE_APP_URL是在 编译 时候,覆盖 axios 默认的 BASE_URL,指向对应的 Node Server,不同的分支也可以有不同的 Value. 与 Amplify 不同, 这里要设置NODE_ENV=production,设置了这个后 npm ci 不会影响install devDependencies下的模块
通过 Amplify 部署
创建 amplify.yaml 文件,修改 build 的脚本
1 | yml复制代码version: 0.2 |
当推送到对应的分支后,Amplify 会调用这个脚本执行编译,打包和部署
设置环境相关的变量
这里的 VUE_APP_URL 是在 编译 时候,覆盖 axios 默认的 BASE_URL,指向对应的 Node Server,不同的分支也可以有不同的 Value
- 注意不要添加
NODE_ENV=production,设置了这个后 npm ci 不会install devDependencies下的模块,会导致npm run build报错无法找到vue-cli-service Vue的Webpack会根据--mode [staging | production ]找到对应的.env.\*文件, 在这些中再声明NODE_ENV=production
创建 Amplify 角色
创建 Amplify APP 时,好像没有自动创建关联的 Role, 我手动创建了一个
部署到 Oracle CentOS 8 服务器中的 Nginx 下
- 配置 Nginx 支持单个端口对应多个二级域名的静态服务
- 编辑 /etc/nginx/nginx.conf 支持同一个端口,不同的静态服务器
1 | ini复制代码server { |
建立对应的目录,在目录下放测试 html
- 修改
Cloudflare,添加三条A记录,支持VPS的IP - 通过
Let's Encrypt修改nginx的https支持
安装certbot见Node Server的部署
1 | shell复制代码certbot -nginx |
- 在 .github/workflows 下添加 Github Actions, 编写 Github Actions 部署脚本
注意不要添加 NODE_ENV=production,设置了这个后 npm ci 不会 install devDependencies 下的模块,会导致 npm run build 报错无法找到 vue-cli-service
Vue 的 Webpack 会根据 –mode [staging | production ] 找到对应的 .env.* 文件, 在这些中再声明 NODE_ENV=production
3. 在 Github 的仓库设置中,给 Actions 用到的添加加密的 Secrets
DEPLOY_ORACLE=/usr/share/nginx/html
后端运行环境和 Heroku/EC2 部署
运行时根据环境加载配置项
- 在 package.json 的脚本中通过 NODE_ENV 定义环境 [development, test, staging, production]
- 安装 dotenv 模块, 该模块通过 NODE_ENV 读取工程根目录下对应的 .env.[${NODE_ENV}] 文件, 加载该环境下的环境变量
- 按 topic[cognito, postgres, server, etc] 定义不同的配置, 并把 process.env.IS_ONLINE_PAYMENT 之类字符串形式的转成不同的变量类型
- 可以把环境变量配置在三个不同的level
- 运行机器的 SHELL 环境变量中, 主要是定义环境的 NODE_ENV 和其他敏感的密码和密钥
- 在以 .${NODE_ENV} 结尾的 .env 文件中
- 最后汇总到 src/config 目录下按 topic 区分的配置信息
- 代码的其他地方读取的都是 src/config 下的配置信息
- 在后端环境下设置 NODE_ENV 有一个副作用,在 Typescript 编译打包前
** 如果 NODE_ENV 设置为 production, npm ci 不会安装 devDependencies 中的依赖包,如果在运行的 EC2 上编译打包,编译会报错。所以打包编译我放在了Github Actions 的容器中了,所以避免了这个问题 **
We have installed all our packages using the –save-dev flag. In production, if we use npm install –production these packages will be skipped.
Amplify 创建线上用户库
- 在前端,通过 Amplify 添加线上环境
1 | shell复制代码amplify env add prod |
- 检查状态, 并部署到
Amplify后台
1 | shell复制代码amplify status |
1 | shell复制代码amplify push |
进入 AWS Cognito 后台, 会增加一个线上的 UserPool
- 添加
foodtruck2826b8ad2826b8adCreateAuthChallenge-prod的短信权限 - 修改线上配置文件中
AWS_COGNITO_USER_POOL_ID
1 | arduino复制代码// .env.oracle |
主要区分三个模块在不同环境的部署
- Postgres 数据库
- 支付
- 用户鉴权
修改 HTTPS 的证书加载
Heroku 部署
Heroku 是我们介绍的三种 CI/CD 流程中最简单的方式
- 创建一条 Pipeline, 在 Pipeline 下创建 staging 和 production 两个应用
- 在 APP 的设置里关联 Github 上对应的仓库和分支
- APP staging 选择 heroku-staging 分支
- APP production 选择 heroku-production 分支
- 为每个 APP 添加 heroku/nodejs 编译插件
1 | shell复制代码heroku login -i |
- 设置运行时的环境变量
这里通过 SERVER 这个运行时的环境变量,告诉 index.ts 不要加载 https 的服务器, 而是用 http 的服务器。
** Heroku 的 API 网关自己已支持 https,后端起的 node server 在内网里是 http, 所以要修改代码 换成 http server,否者会报 503 错误** - 修改 index.ts 文件,在 Heroku 下改成 HTTP
- APP production 一般不需要走 CI/CD 的流程,只要设置 NODE_ENV=production,然后在 APP staging 验证通过后, promote 就可以完成快速部署。
- 查看 heroku 上的日志
1 | shell复制代码heroku logs --tail -a staging-api-food-truck |
AWS EC2 部署
在 AWS 上搭建环境和创建用户和角色
CodeDeploy
We’ll be using CodeDeploy for this setup so we have to create a CodeDeploy application for our project and two deployment groups for the application. One for staging and the other for production.
- To create the api-server CodeDeploy application using AWS CLI, we run this on our terminal:
1 | shell复制代码aws deploy create-application \ |
- Before we run the cli command to create the service role, we need to create a file with IAM specifications for the role, copy the content below into it and name it code-deploy-trust.json
1 | json复制代码{ |
- We can now create the role by running:
1 | shell复制代码aws iam create-role \ |
- After the role is created we attach the AWSCodeDeployRole policy to the role
1 | shell复制代码aws iam attach-role-policy \ |
- To create a deployment group we would be needing the service role ARN.
1 | shell复制代码aws iam get-role \ |
The ARN should look something like arn:aws:iam::403593870368:role/CodeDeployServiceRole
6. Let’s go on to create a deployment group for the staging and production environments.
1 | shell复制代码aws deploy create-deployment-group \ |
1 | shell复制代码aws deploy create-deployment-group \ |
进入 Console -> Code Deploy 确认
创建 S3 Bucket
创建一个名为 node-koa2-typescript 的 S3 Bucket
1 | shell复制代码aws s3api create-bucket --bucket node-koa2-typescript --region ap-northeast-1 |
Create and Launch EC2 instance
完整的演示,应该创建 staging 和 production 两个 EC2 实例,为了节省资源,这里只创建一个实例
- 创建一个具有访问 S3 权限的角色 EC2RoleFetchS3
- In this article, we will be selecting the Amazon Linux 2 AMI (HVM), SSD Volume Type.
- 绑定上面创建的角色,并确认开启80/22/3001/3002几个端口
- 添加 tag,key 是 Name,Value 是 production
- 导入用于 ssh 远程登入的公钥
- 通过 ssh 远程登入 EC2 实例,安装 CodeDeploy Agent
安装步骤详见 CodeDeploy Agent
- 通过 ssh 安装 Node.js 的运行环境
- 通过 NVM 安装 Node.js
1 | shell复制代码curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash |
- 安装 PM2 管理 node 的进程
1 | shell复制代码npm i pm2 -g |
- 在项目的根目录下创建 ecosystem.config.js 文件
1 | javascript复制代码module.exports = { |
- 在 EC2 的实例 ec2-user 账号下设置环境变量, 编辑 ~/.bash_profile
1 | bash复制代码export NODE_ENV=production |
这里放置 NODE_ENV 和具有敏感信息的环境变量, 这里 SERVER=AWS 只是演示
创建用于 Github Actions 部署脚本的用户组和权限
- 在 IAM 中创建以一个 CodeDeployGroup 用户组,并赋予 AmazonS3FullAccess and AWSCodeDeployFullAccess 权限
- 在 CodeDeployGroup 添加一个 dev 用户,记录下 Access key ID 和 Secret access key
编写 Github Actions 脚本
- 在工程的根目录下创建 .github/workflows/deploy-ec2.yaml 文件
deploy-ec2.yaml 的作用是,当修改的代码提交到 aws-staging 或 aws-production,触发编译,打包,并上传到 S3 的 node-koa2-typescript bucket, 然后再触发 CodeDeploy 完成后续的部署。所以这个 Github Action 是属于 CI 的角色,后面的 CodeDeploy 是 CD 的角色。 - 在 Github 该项目的设置中添加 Environment secrets, 将刚才 dev 用户的 Access key ID 和 Secret access key 添加进Environment secrets
添加 appspec.yml 及相关脚本
CodeDeploy 从 S3 node-koa2-typescript bucket 中获取最新的打包产物后,上传到 EC2 实例,解压到对应的目录下,这里我们指定的是 /home/ec2-user/api-server。CodeDeploy Agent 会找到该目录下的 appspec.yml 文件执行不同阶段的 Hook 脚本
1 | yml复制代码version: 0.0 |
aws-ec2-deploy-scripts/application-start.sh 启动了 Node.js 的服务
1 | shell复制代码#!/usr/bin/env bash |
在 EC2 实例下安装免费的域名证书,步骤详见Certificate automation: Let’s Encrypt with Certbot on Amazon Linux 2
- 去 Cloudflare 添加 A 记录指向这台 EC2 实例,指定二级域名是 aws-api
- 安装配置 Apache 服务器,用于证书认证
- Install and run Certbot
1 | shell复制代码sudo certbot -d aws-api.magicefire.com |
根据提示操作,最后证书生成在 /etc/letsencrypt/live/aws-api.magicefire.com/ 目录下
4. 因为我们启动 Node 服务的账号是 ec2-user, 而证书是 root 的权限创建的,所以去 /etc/letsencrypt 给 live 和 archive 两个目录添加其他用户的读取权限
1 | shell复制代码sudo -i |
- Configure automated certificate renewal
部署和验证
- 如果没有 aws-production 分支,先创建该分支,并切换到该分支下,合并修改的代码,推送到Github
1 | shell复制代码git checkout -b aws-production |
- 触发 Github Actions
- 编译,打包并上传到 S3 后,触发 CodeDeploy
- 完成后在浏览器里检查
或用 curl 在命令行下确认
1 | shell复制代码curl https://aws-api.magicefire.com:3002/api/v1/health |
- 因为我们没有创建 EC2 的 staging 实例,如果推送到 aws-staging 分支,CodeDeploy 会提示以下错误
过程回顾
Postgres VPS 和 Heroku 部署[Server]
在 Heroku 上部署 Postgres
- Provisioning Heroku Postgres
1 | shell复制代码heroku addons |
- Sharing Heroku Postgres between applications
1 | shell复制代码heroku addons:attach my-originating-app::DATABASE --app staging-api-node-server |
- 导入商品到线上 table
1 | shell复制代码npm run import-goods-heroku-postgre |
在 VPS 上部署 Postgres
编外
支持 Swagger
本文转载自: 掘金