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-node
repository onGithub
first
- 初始化本地
git
仓库, 并推送到Github
仓库中
initialize the local repository, and push it onto the remote repository underGithub
1 | shell复制代码mkdir backend && cd backend |
- 添加
.gitignore
add.gitignore
file
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.json
scripts 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.json
and 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-lib
on 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
本文转载自: 掘金