上一篇文章我们在腾讯云 Serverless 上部署了一个最基本的 Nest 框架,现在让它能连接上 Mysql 数据库吧
因为没有购买 RDS 的原因,所以这篇文章就介绍云函数怎么使用内网连接在腾讯云 ECS 上的 Mysql 服务,前提是云服务器与云函数在同一个地区以及在同一个子网环境下
# 云服务与 Mysql 服务
# 查看云服务器的内网 IP
买云服务器都会有一个外网 IP 和一个内网 IP, 外网 IP 用来和外界交互,而内网 IP 就是用来和内网环境下的服务交互了,简单说就是同是在腾讯云购买的服务,如果在同一个地域和同一个子网下,那么可以使用内网交互,速度比外网交互快得多,延迟也更低。
查看内网可以使用 ifconfig
命令
$ ifconfig | |
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 | |
inet 172.17.16.13 netmask 255.255.240.0 broadcast 172.17.31.255 | |
inet6 fe80::5054:ff:fecf:7581 prefixlen 64 scopeid 0x20<link> | |
ether 52:54:00:cf:75:81 txqueuelen 1000 (Ethernet) | |
RX packets 158609312 bytes 30728239812 (28.6 GiB) | |
RX errors 0 dropped 0 overruns 0 frame 0 | |
TX packets 154639972 bytes 57536005501 (53.5 GiB) | |
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 | |
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 | |
inet 127.0.0.1 netmask 255.0.0.0 | |
inet6 ::1 prefixlen 128 scopeid 0x10<host> | |
loop txqueuelen 1000 (Local Loopback) | |
RX packets 89181934 bytes 97925334102 (91.2 GiB) | |
RX errors 0 dropped 0 overruns 0 frame 0 | |
TX packets 89181934 bytes 97925334102 (91.2 GiB) | |
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 |
这里查到内网 IP 是 172.17.16.13
# 创建 Mysql 用户名及授权
在云服务上已安装好 Mysql8, 安装方式见 Ubuntu 二进制方式安装 Mysql-8.0.21 (x86_64) 详细记录
使用 root 用户登录创建一个名为 manager
的库
mysql> create database manager default charset utf8mb4 collate utf8mb4_general_ci; | |
Query OK, 1 row affected, 2 warnings (0.01 sec) |
创建一个新用户 managerserverless
并授权拥有该库的一切权限
mysql> create user 'managerserverless'@'%' identified by '123456'; | |
Query OK, 0 rows affected (0.05 sec) | |
mysql> grant all privileges on manager.* to 'managerserverless'@'%' with grant option; | |
Query OK, 0 rows affected (0.01 sec) | |
mysql> flush privileges; | |
Query OK, 0 rows affected (0.05 sec) |
尝试使用 managerserverless
用户登录一下
➜ /root mysql -umanagerserverless -p123456 | |
mysql: [Warning] Using a password on the command line interface can be insecure. | |
Welcome to the MySQL monitor. Commands end with ; or \g. | |
Your MySQL connection id is 10411 | |
Server version: 8.0.21 MySQL Community Server - GPL | |
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. | |
Oracle is a registered trademark of Oracle Corporation and/or its | |
affiliates. Other names may be trademarks of their respective | |
owners. | |
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. | |
mysql> show databases; | |
+--------------------+ | |
| Database | | |
+--------------------+ | |
| information_schema | | |
| manager | | |
+--------------------+ | |
2 rows in set (0.01 sec) |
至此云服务器和 Mysql 数据库就准备好了,接下来修改 Nest 项目,集成 Typeorm 到 Nest 中
# Nest9.0 集成 Typeorm
# 安装 Typeorm
$ npm install --save @nestjs/typeorm typeorm mysql2 |
# 使用腾讯云 serverless 需要注意的一点 - 层管理
腾讯云 serverless 的云函数只支持总代码量小于 250MB, 这个是要算 node_modules
文件夹的,随着项目开发的逐渐进展,所要安装的库文件越来越多越来越大,250MB 是远远不够的.
其实云函数所应用到的库文件只包括 package.json
的 dependencies
部分, devDependencies
完全无用,所以要另外创建一个文件夹用来放只安装 dependencies
的 node_modules
文件夹.
创建一个新的文件夹,将 package.json
复制进去,然后执行 npm install --production
即可
随后在腾讯云 Serverless 控制台 - 层中选择 manager-layer
新建一个版本,将该文件夹整个上传就好了.
# 修改.env 和 serverless.yml 文件
.env 添加 Mysql 配置项
#Mysql配置 | |
MYSQL_HOST=172.17.16.13 | |
MYSQL_USER=managerserverless | |
MYSQL_PASSWORD=123456 | |
MYSQL_DATABASE=manager |
serverless.yml 修改 layer
的版本号和 faas
的 environments
部分
app: manager | |
stage: dev | |
component: http | |
name: manager | |
inputs: | |
src: | |
dist: ./ | |
hook: npm run build | |
exclude: | |
- .env | |
- node_modules | |
src: ./ | |
faas: | |
runtime: Nodejs16.13 | |
framework: nestjs | |
name: '${name}-${stage}' | |
eip: false | |
timeout: 3 | |
memorySize: 512 | |
tags: [] | |
environments: | |
- key: MYSQL_HOST | |
value: ${env:MYSQL_HOST} | |
- key: MYSQL_USER | |
value: ${env:MYSQL_USER} | |
- key: MYSQL_DATABASE | |
value: ${env:MYSQL_DATABASE} | |
- key: MYSQL_PASSWORD | |
value: ${env:MYSQL_PASSWORD} | |
vpc: # 私有网络配置 | |
vpcId: 'vpc-b5776770' # 私有网络的 Id | |
subnetId: 'subnet-asy3z93z' # 子网 ID | |
layers: | |
- name: manager-layer | |
version: 3 | |
apigw: | |
protocols: | |
- http | |
- https | |
timeout: 60 | |
customDomains: [] | |
region: ${env:REGION} |
# 集成 Typeorm
# 第一步。集成不同环境的配置变量
应用程序通常在不同的环境中运行。根据环境的不同,应该使用不同的配置设置。
例如,本地开发时访问本地数据库,部署到 serverless 后访问云服务器上的数据库服务.
那么就需要将线上环境和本地环境区分开,采取不同的环境变量配置
安装 nestjs-config
和 cross-env
$ yarn add nestjs-config | |
$ yarn add cross-env -D |
修改 package.json
的 scripts
项
"dev": "npm run start:dev", | |
"start:dev": "cross-env NODE_ENV=local nest start --watch", |
修改 app.module.ts
, 集成 nestjs-config
和 Typeorm
import { Module } from '@nestjs/common'; | |
import { AppController } from './app.controller'; | |
import { AppService } from './app.service'; | |
import { HttpModule } from '@nestjs/axios'; | |
import { TypeOrmModule } from '@nestjs/typeorm'; | |
import {ConfigModule,ConfigService } from 'nestjs-config'; | |
import * as path from 'path'; | |
const ENV = process.env.NODE_ENV; | |
@Module({ | |
imports: [ | |
/* | |
* 配置文件从 src/config 下的 *.ts 或 *.js 文件取,配置文件需要导出为一个模块 | |
* 如果环境变量 NODE_ENV 为 local, 则会取.env.local 里的环境变量,如果 NODE_ENV 不存在,则取.env 文件的环境变量 | |
*/ | |
ConfigModule.load(path.resolve(__dirname,'config','**/!(*.d).{ts,js}'),{ | |
path:path.resolve(process.cwd(),!ENV ? '.env' : `.env.${ENV}`) | |
}), | |
HttpModule, | |
/* | |
* 采用异步的方式集成 Typeorm, 配置文件在 src/config/database.ts 中 | |
*/ | |
TypeOrmModule.forRootAsync({ | |
useFactory: async (config: ConfigService) => config.get('database'), | |
inject: [ConfigService], | |
}), | |
], | |
controllers: [AppController], | |
providers: [AppService], | |
}) | |
export class AppModule {} |
我们在 src
目录下创建 config
目录,里面添加一个 database.ts
文件
#database.ts | |
export default { | |
type: 'mysql', | |
host: process.env.MYSQL_HOST, | |
username: process.env.MYSQL_USER, | |
password: process.env.MYSQL_PASSWORD, | |
database: process.env.MYSQL_DATABASE, | |
logging: process.env.MYSQL_LOGGING === 'true', | |
synchronize: process.env.MYSQL_SYNC === 'true', | |
autoLoadEntities: true, | |
entities: ['dist/**/*.entity{.ts,.js}'], | |
port: parseInt(process.env.MYSQL_PORT), | |
keepConnectionAlive: true | |
}; |
Typeorm 关于 Mysql 的配置项说明
同时创建 .env.local
文件,添加相应配置项 ( .env
里同样要添加)
以下是我的配置
//.env.local | |
#Mysql配置 | |
MYSQL_HOST=127.0.0.1 | |
MYSQL_USER=managerserverless | |
MYSQL_PASSWORD=123456 | |
MYSQL_PORT=33060 | |
MYSQL_DATABASE=manager | |
MYSQL_LOGGING=true | |
MYSQL_SYNC=true |
这样就可以实现通过一个环境变量 NODE_ENV
切换不同环境的配置文件了,如果后期再加一个环境也很方便
# 第二步。编写第一个 entity
创建一个 options
表,可以使用 Typeorm 的 entity
自动创建相应的表.
首先创建 options
模块,控制器和 service
$ nest g module options | |
$ nest g co options | |
$ nest g s options |
在生成的 src/options
下创建 entity
文件夹,创建 options.entity.ts
文件
import {Entity, Column, PrimaryGeneratedColumn,Index} from 'typeorm'; | |
@Entity("options") | |
export class Options { | |
@PrimaryGeneratedColumn({ | |
unsigned:true, | |
}) | |
id: number; | |
@Index() | |
@Column({ | |
type: "varchar", | |
length: 255, | |
default: "", | |
comment: "配置key" | |
}) | |
key: string; | |
@Column({ | |
type: "varchar", | |
length: 2000, | |
default: "", | |
comment: "配置值" | |
}) | |
value: string; | |
@Index() | |
@Column({ | |
type: "varchar", | |
length: 255, | |
default: "", | |
comment: "配置前缀" | |
}) | |
prefix: string; | |
} |
接下来执行 npm run dev
提示如下信息,表自动就创建好了
query: SELECT VERSION() AS `version` | |
query: START TRANSACTION | |
query: SELECT DATABASE() AS `db_name` | |
query: SELECT `TABLE_SCHEMA`, `TABLE_NAME` FROM `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = 'manager' AND `TABLE_NAME` = 'options' | |
query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'manager' AND `TABLE_NAME` = 'typeorm_metadata' | |
query: CREATE TABLE `options` (`id` int UNSIGNED NOT NULL AUTO_INCREMENT, `key` varchar(255) NOT NULL COMMENT '配置key' DEFAULT '', `value` varchar(2000) NOT NULL COMMENT '配置值' DEFAULT '', `prefix` varchar(255) NOT NULL COMMENT '配置前缀' DEFAULT '', INDEX `IDX_4d886cd47aa57e872fbf2ab3c1` (`key`), INDEX `IDX_5fc0b299d23181f6f2384bb735` (`prefix`), PRIMARY KEY (`id`)) ENGINE=InnoDB | |
query: COMMIT |
至此,配置环境变量和集成 Typeorm 已经完成,现在项目可以访问到服务器上的 Mysql 数据库了.
# 第三步。编写 options 接口操作数据库
接下来添加几个接口验证一下
修改 options.module.ts
/* | |
* options.module.ts | |
*/ | |
import { Module } from '@nestjs/common'; | |
import { OptionsController } from './options.controller'; | |
import { OptionsService } from './options.service'; | |
import { Options } from "./entity/options.entity"; | |
import { TypeOrmModule } from "@nestjs/typeorm"; | |
@Module({ | |
imports:[ | |
TypeOrmModule.forFeature([Options]) | |
], | |
controllers: [OptionsController], | |
providers: [OptionsService] | |
}) | |
export class OptionsModule {} |
修改 options.service.ts
/* | |
* options.service.ts | |
*/ | |
import { Injectable } from '@nestjs/common'; | |
import { InjectRepository } from "@nestjs/typeorm"; | |
import { Options } from "./entity/options.entity"; | |
import { Repository } from "typeorm"; | |
type optionType = { | |
key:string, | |
value:string, | |
prefix:string|null | |
}; | |
@Injectable() | |
export class OptionsService { | |
constructor( | |
@InjectRepository(Options) | |
private optionsRepository: Repository<Options> | |
) {} | |
async getAll():Promise<Options[]>{ | |
return this.optionsRepository.find(); | |
} | |
async getOne(key:string):Promise<Options>{ | |
return this.optionsRepository.findOne({where:{key}}); | |
} | |
async update(option:Options,value:string):Promise<boolean>{ | |
option.value = value; | |
await this.optionsRepository.save(option); | |
return true; | |
} | |
async add(data:optionType):Promise<Options>{ | |
const option = new Options(); | |
option.key = data.key; | |
option.value = data.value; | |
option.prefix = data.prefix?data.prefix:""; | |
return await this.optionsRepository.save(option); | |
} | |
} |
修改 options.controller.ts
import { Controller,Get,Post,Body } from '@nestjs/common'; | |
import { Options } from "./entity/options.entity"; | |
import { OptionsService } from "./options.service"; | |
type optionType = { | |
key:string, | |
value:string, | |
prefix:string|null | |
}; | |
@Controller('options') | |
export class OptionsController { | |
constructor( | |
private readonly optionsService: OptionsService, | |
) {} | |
@Get() | |
async getAllOptions():Promise<Options[]>{ | |
return await this.optionsService.getAll(); | |
} | |
@Post("set") | |
async setOption(@Body() data:optionType):Promise<Options>{ | |
let option = await this.optionsService.getOne(data.key); | |
if(option){ | |
await this.optionsService.update(option,data.value); | |
}else{ | |
option = await this.optionsService.add(data); | |
} | |
return option; | |
} | |
} |
添加了 2 个接口
GET /options 返回 options 列表
POST /options/set 设置 options, 根据 key 查询,有则修改,无则新增
先本地测试一遍,没问题就可以部署了
部署之前依旧更新一下 "层", 同时修改 serverless.yml
文件的 layer
版本号 (只要 package.json 的 dependencies 内容有变动就需要更新一下)
执行 npm run build && npm run deploy
> npm run build && sls deploy | |
> manager@0.0.1 prebuild | |
> rimraf dist | |
> manager@0.0.1 build | |
> nest build | |
serverless ⚡tencent | |
Action: "deploy" - Stage: "dev" - App: "manager" - Name: "manager" | |
apigw: | |
apiList: | |
- | |
apiId: api-l764cd57 | |
apiName: http_api | |
authType: NONE | |
businessType: NORMAL | |
created: true | |
internalDomain: | |
isBase64Encoded: false | |
method: ANY | |
path: / | |
url: https://service-mlmh4ts7-1252902543.sh.apigw.tencentcs.com/release/ | |
environment: release | |
id: service-mlmh4ts7 | |
subDomain: service-mlmh4ts7-1252902543.sh.apigw.tencentcs.com | |
url: https://service-mlmh4ts7-1252902543.sh.apigw.tencentcs.com/release/ | |
faas: | |
name: manager-dev | |
namespace: default | |
runtime: Nodejs16.13 | |
type: web | |
region: ap-shanghai | |
应用控制台: https://serverless.cloud.tencent.com/apps/manager/manager/dev | |
91s »manager» 执行成功 |
使用 curl
工具测试一下
➜ curl --location --request POST 'https://service-mlmh4ts7-1252902543.sh.apigw.tencentcs.com/options/set' \ | |
--header 'Content-Type: application/json' \ | |
--data '{"key":"first","value":"first value"}' | |
{"id":1,"key":"first","value":"first value","prefix":""}# | |
➜ curl --location --request GET 'https://service-mlmh4ts7-1252902543.sh.apigw.tencentcs.com/options' | |
[{"id":1,"key":"first","value":"first value","prefix":""}]# |
测试成功!