前端构建 DevOps - 搭建 DevOps 基础平台(上)

前言

在上一个博客中,已经通过 Egg 对 Gitlab Api 进行了基础的封装,本文将会围绕 DevOps 流程介绍项目设计(偏后台),需要读者具备一定的后端知识储备。

此系列即是持续交付项目的教程亦可作为 node 开发的教程来使用,从开发-测试-构建-部署的一整套 DevOps 项目

一共包含如下 2 个系列,分为前后端两个模块

后端模块

  1. DevOps - Gitlab Api使用(已完成,点击跳转)
  2. DevOps - 搭建 DevOps 基础平台(已完成 30%)
  3. DevOps - Gitlab CI 流水线构建
  4. DevOps - Jenkins 流水线构建
  5. DevOps - Docker 使用
  6. DevOps - 发布任务流程设计
  7. DevOps - 代码审查卡点
  8. DevOps - Node 服务质量监控

前端模块

  1. DevOps - H5 基础脚手架
  2. DevOps - React 项目开发

后期可能会根据 DevOps 项目的实际开发进度对上述系列进行调整

DevOps 设计

简单分析一下此项目研发流程的架构,接下来再做后续的步骤(剧本已写好,就看怎么演了

项目需求分析(系统开发的目的跟结果)

  1. 从项目开发-测试-构建-部署一整套流程,简化交付成本
  2. 研发流程中加入能效概念(研发时间-测试时间-总体交付时间-bug 率及修复时间),作为项目提效的一个参考标准(影响因素太多,仅供参考)
  3. 合理的提测卡点,减少无效的提测,减轻测试负担,提高流程闭环质量
  4. 提供线上监控,分析每个版本使用率,报错率,提高项目研发质量
  5. 提供快速回滚指定版本功能,确保新版本崩溃情况下能够快速恢复服务

此项目是从零开发,在正式开发之前,需要先将需求理清,以免设计出现严重缺陷,造成后期开发或拓展困难(路可以走的慢,但不要走偏)。

流程设计

image

如上图所示,将上一篇的发布流程更进一步的细化可以分为下面 4 类:

  1. 单项目发布流程(一个需求只需要一个工程完成)
  2. 生产环境出问题,快速回滚功能
  3. 集成项目发布流程(一个需求可能会有多个工程参与开发、发布)
  4. Bug 修复发布流程(无需求,需要快速修复线上已知但不紧急 bug 的发布流程)

任务流的设计其实非常复杂,为了加快交付第一版,先将任务流固定为以上 4 类,减少开发量,后期会添加或者修改某个流程

数据库设计

sequelize 的使用

sequelize 提供了 sequelize-cli 工具来实现 Migrations,我们也可以在 egg 项目中引入 sequelize-cli(具体介绍参考 sequelize 操作)。

如果你参考上一篇博客已经将环境搭建完毕,可以使用 npm install --save-dev sequelize-cli 安装 sequelize-cli 工具,再通过下面配置生成需要的表。

use strict';
const path = require('path');

module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),
};

上述是 .sequelizerc 配置,请放在项目根目录下

npx sequelize init:config
npx sequelize init:migrations

执行完后会生成 database/config.json 文件和 database/migrations 目录,修改一下 database/config.json 中的内容,将其改成项目中使用的数据库配置:

{
  "development": {  // 本地数据库,其他环境数据库,照着例子自己改
    "username": "root",
    "password": "123456",
    "database": "devops_dev",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
}

再通过 npx sequelize migration:generate --name=init-users 来创建数据库表

module.exports = { // 为了减少工作量,权限我们直接使用 gitlab 的,所以我们只需要落库以下字段
  up: async (queryInterface, Sequelize) => {
    const { INTEGER, DATE, STRING } = Sequelize;
    await queryInterface.createTable('users', {
      id: { type: INTEGER, primaryKey: true, },
      name: STRING(30),
      username: STRING(30),
      email: STRING(100),
      avatar_url: STRING(200),
      web_url: STRING(200),
      created_at: DATE,
      updated_at: DATE,
    });
  },
  down: async queryInterface => {
    await queryInterface.dropTable('users');
  },
};

最后执行 migrate 进行数据库变更

# 升级数据库
npx sequelize db:migrate
# 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
# npx sequelize db:migrate:undo
# 可以通过 `db:migrate:undo:all` 回退到初始状态
# npx sequelize db:migrate:undo:all

设计基础表

[图片上传失败...(image-1e5ecf-1605149145994)]

将 gitlab project 与 branch 常用的数据落库到本地,再根据项目需求新增字段,大概的表结构如上图所示

结合上述项目流程设计,说明一下表结构关系

  1. 工程表 project 会管理多个分支 branch,可以查询当前工程下所有分支的状态(是否被提测,是否存在流程中)
  2. 创建一个流程(等同于需求)关联多个 branch 开发
  3. 流程创建完之后必走完所有步骤直至完结(开发-测试-预发-生产
  4. 当 branch 被一个流程关联之后,既被所锁定,不会再次被加入到其他流程(需求锁定隔离,保证开发过程不会有干扰
  5. 在流程的提测步骤中,可以针对不同 branch 进行多次提测(复杂需求通过分批提测,完成预期目标)
  6. 当流程中所有 branch 的状态都已测试通过之后,该流程状态才进入下一个阶段,否则一直停留在测试阶段

测试记录表没放上去,暂且先把上述功能开发完毕,再结合后面的分支管理进行修改

DevOps 开发

添加接口全局返回参数

import { Controller } from "egg";

export default class BaseController extends Controller {
  get user() {
    return this.ctx.user;
  }

  success(data) {
    this.ctx.body = {
      code: 0,
      data,
    };
  }

  error({ code, data, message }) {
    // 根据业务返回不同的错误 code,提供给前端做业务判断处理
    this.ctx.body = {
      code,
      data,
      message,
    };
  }
}

定义全局返回参数基础类,业务 Controller 继承基础类,前端可以根据返回的 code 值进行业务判断

jwt 权限验证

上一篇介绍了从 Gitlab 获取 access_token 来操作 open api 的方法,但我们还是需要将用户信息从在本地落库,方便我们后期使用

项目的权限验证,采取简单的 jwt 来使用,将用户数据及 access_token 保存起来,后期完成第一阶段的目标之后再进行改进

具体的 egg-jwt 的使用可以参考(egg-jwt 使用),这里直接附上业务侧的代码供参考:

const excludeUrl = ["/user/getUserToken"]; // 请求白名单,过滤不需要校验的请求路径

export default () => {
  const jwtAuth = async (ctx, next) => {
    if (excludeUrl.includes(ctx.request.url)) {
      return await next();
    }
    const token = ctx.request.header.authorization;
    if (token) {
      try {
        // 解码token
        const deCode = ctx.app.jwt.verify(
          token.replace("Bearer ", ""), // jwt 中间件验证的时候,需要去掉 Bearer
          ctx.app.config.jwt.secret
        );
        ctx.user = deCode;
        await next();
      } catch (error) {
        ctx.status = 401;
        ctx.body = {
          code: 401,
          message: error.message,
        };
      }
      return;
    }
    ctx.status = 401;
    ctx.body = {
      code: 401,
      message: "验证失败",
    };
    return;
  };
  return jwtAuth;
};

以上是全局拦截 jwt 权限中间件,验证权限之后,将用户数据存入 ctx 供后续业务侧调用。中间件的具体使用可以参考 egg 中间件

// Controller
import { Post, Prefix } from "egg-shell-decorators";
import BaseController from "./base";

@Prefix("user")
export default class UserController extends BaseController {
  @Post("/getUserToken")
  public async getUserToken({
    request: {
      body: { params },
    },
  }) {
    const { ctx, app } = this;
    const { username, password } = params;

    // gitlab 获取 access_token
    const userToken = await ctx.service.user.getUserToken({
      username,
      password,
    });

    // gitlab 获取用户信息
    const userInfo = await ctx.service.user.getUserInfo({
      accessToken: userToken.access_token,
    });

    // 用户数据本地落库
    ctx.service.user.saveUser({
      userInfo,
    });

    // 将用户信息及 token 使用 jwt 注册
    const token = app.jwt.sign(
      {
        userToken,
        userInfo,
      },
      app.config.jwt.secret
    );
    
    ctx.set({ authorization: token }); // 设置 headers
    this.success(userInfo);
  }
}

// Service
import { Service } from "egg";

export default class User extends Service {
  // 使用 gitlab api 获取 access_token
  public async getUserToken({ username, password }) {
    const { data: token } = await this.ctx.helper.utils.http.post(
      "/oauth/token",
      {
        grant_type: "password",
        username,
        password,
      }
    );
    if (token && token.access_token) {
      return token;
    }
    return false;
  }

  // 使用 gitlab api 获取 gitlab 用户信息
  public async getUserInfo({ accessToken }) {
    const userInfo = await this.ctx.helper.api.gitlab.user.getUserInfo({
      accessToken,
    });
    return userInfo;
  }

  // 用户信息落库
  public async saveUser({ userInfo }) {
    const { ctx } = this;
    const {
      id,
      name,
      username,
      email,
      avatar_url: avatarUrl,
      web_url: webUrl,
    } = userInfo;

    // 查询用户是否已经落库
    const exist = await ctx.model.User.findOne({
      where: {
        id,
      },
      raw: true,
    });
    if (exist) return;

    // 创建用户信息
    ctx.model.User.create({
      id,
      name,
      username,
      email,
      avatarUrl,
      webUrl,
    });
  }
}

上述是服务端 jwt 的使用实例,在全局中间件拦截的时候可以解析出想要的信息来后续使用,客户端的实例,我们在 react 项目中单独说明。

以上是数据库建表以及用户、权限操作的实例与简介,此系列下一篇等基本的任务流开发完毕后再推出,预计 2 周左右

尾声

此项目是从零开发,后续此系列博客会根据实际开发进度推出,项目完成之后,会开放部分源码供各位同学参考。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容