上一篇文章我们在腾讯云 Serverless 上部署了一个最基本的 Nest 框架,现在让它能连接上 Mysql 数据库吧
因为没有购买 RDS 的原因,所以这篇文章就介绍云函数怎么使用内网连接在腾讯云 ECS 上的 Mysql 服务,前提是云服务器与云函数在同一个地区以及在同一个子网环境下

# 云服务与 Mysql 服务

# 查看云服务器的内网 IP

买云服务器都会有一个外网 IP 和一个内网 IP, 外网 IP 用来和外界交互,而内网 IP 就是用来和内网环境下的服务交互了,简单说就是同是在腾讯云购买的服务,如果在同一个地域和同一个子网下,那么可以使用内网交互,速度比外网交互快得多,延迟也更低。

查看内网可以使用 ifconfig 命令

h
$ 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 的库

h
mysql> create database manager default charset utf8mb4 collate utf8mb4_general_ci;
Query OK, 1 row affected, 2 warnings (0.01 sec)

创建一个新用户 managerserverless 并授权拥有该库的一切权限

h
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 用户登录一下

h
➜  /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

h
$ npm install --save @nestjs/typeorm typeorm mysql2

# 使用腾讯云 serverless 需要注意的一点 - 层管理

腾讯云 serverless 的云函数只支持总代码量小于 250MB, 这个是要算 node_modules 文件夹的,随着项目开发的逐渐进展,所要安装的库文件越来越多越来越大,250MB 是远远不够的.

其实云函数所应用到的库文件只包括 package.jsondependencies 部分, devDependencies 完全无用,所以要另外创建一个文件夹用来放只安装 dependenciesnode_modules 文件夹.

创建一个新的文件夹,将 package.json 复制进去,然后执行 npm install --production 即可

随后在腾讯云 Serverless 控制台 - 层中选择 manager-layer 新建一个版本,将该文件夹整个上传就好了.

# 修改.env 和 serverless.yml 文件

.env 添加 Mysql 配置项

n
#Mysql配置
MYSQL_HOST=172.17.16.13
MYSQL_USER=managerserverless
MYSQL_PASSWORD=123456
MYSQL_DATABASE=manager

serverless.yml 修改 layer 的版本号和 faasenvironments 部分

l
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-configcross-env

h
$ yarn add nestjs-config
$ yarn add cross-env -D

修改 package.jsonscripts

n
"dev": "npm run start:dev",
"start:dev": "cross-env NODE_ENV=local nest start --watch",

修改 app.module.ts , 集成 nestjs-configTypeorm

t
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 文件

t
#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 里同样要添加)

以下是我的配置

n
//.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

h
$ nest g module options
$ nest g co options
$ nest g s options

在生成的 src/options 下创建 entity 文件夹,创建 options.entity.ts 文件

t
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 提示如下信息,表自动就创建好了

h
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

t
/*
* 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

t
/*
* 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

t
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

h
> 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 工具测试一下

h
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":""}]#

测试成功!

请我喝杯[咖啡]~( ̄▽ ̄)~*

一个放羊娃 微信支付

微信支付

一个放羊娃 支付宝

支付宝