Published on

Serverless-Devs实践-基于 Node 的前端 CICD

Authors
  • avatar
    Name
    Jason Lam / 林家祥
    Twitter

Serverless Devs 是一个开源开放的 Serverless 开发者平台,致力于为开发者提供强大的工具链体系。通过该平台,开发者可以一键体验多云 Serverless 产品,极速部署 Serverless 项目。本文介绍通过 Serverless-Devs 部署一个前端 CICD 系统。

基本信息

自动将前端项目托管至阿里云对象存储平台,无需服务器及额外运维操作,并实现 CDN 访问加速。

方案架构:

1 (1)

开始之前

Serverless Devs

如果您的开发环境没有 Serverless Devs,请参考Serverless Devs Install-tutorial进行安装。

Docker

本文档涉及本地调试,因此需要开发环境具有 Docker,您可根据您开发平台的不同安装不同版本,可参考Serverless Devs Install-tutorial可选部分。

Github

  • 本文档基于 Github 进行实践,因此不保证在其他代码托管平台的可用性,您可以根据您具体使用的代码平台的 webhook 参数对本文代码进行修改。您需要准备一个前端项目以便进行下一步,本文准备了serverless-cicd-example-vue便于实践
  • 创建密钥对并绑定到 github,可参考使用 SSH 连接到 GitHub - GitHub Docs

配置

RAM 账号

前往RAM 访问控制创建一个子用户,赋予其对象存储服务权限「AliyunOSSFullAccess」及管理函数计算(FC)服务权限「AliyunFCFullAccess」并创建 AccessKey。

image-20210720133222847

对象存储服务(OSS)

前往OSS 管理控制台开通对象存储服务,并创建一个 Bucket,Bucket 名称为 serverless-cicd,存储地域为华南 1(深圳),其他参数为默认值。您可以选择不同的地域及 Bucket 名称,但需要对代码进行相应修改

函数计算服务(FC)

前往函数计算控制台开通函数计算服务,请注意,函数计算服务分服务地域进行管理,您可能需要切换地域才能查看相应的服务及函数。

日志服务(SLS)

前往日志服务开通日志服务,并创建一个名为 serverless-cicd-log 的 Project,创建成功后会提示是否创建 logStore,我们选择确认。

image-20210720135617456

image-20210720135627586

logStore 我们填写名称,其他保持默认。

image-20210720135804507

创建成功后会提示是否立即接入数据,我们选择取消。

image-20210720152752200

Serverless-Devs 密钥配置

参考配置阿里云密钥S config,将前文添加的子用户 AccessKey 配置到 S 中。

Serverless-Devs 实践-基于 Node 的前端 CICD

初始化项目

执行s init来创建项目,我们选择Alibaba Cloud Serverless ➡️ HTTP Function-Node.js 12 Example ➡️ 输入文件夹名称

修改 s.yaml

注意双##注释内容与项目生成配置文件的差异,主要有增加日志配置,修改超时时长以及修改 http 触发器合法请求方式:

services:
  fc-deploy-test: #  服务名称
    component: devsapp/fc # 组件名称
    props: #  组件的属性值
      region: cn-shenzhen ## 此处可不修改
      service:
        name: fc-deploy-cicd
        description: 'demo for fc-node-cicd component'
        internetAccess: true
        ## 增加日志配置
        logConfig:
          enableRequestMetrics: true
          logstore: serverless-cicd-log
          project: serverless-cicd-log
      function:
        name: http-trigger-function
        description: this is a test
        runtime: nodejs12
        codeUri: ./
        handler: index.handler
        memorySize: 128
        timeout: 180 ## 增加时长,避免因为网络问题超时
      triggers:
        - name: httpTrigger
          type: http
          config:
            authType: anonymous
            methods:
              - GET
              - POST ## Github Webhook为POST方式
      customDomains:
        - domainName: auto
          protocol: HTTP
          routeConfigs:
            - path: /*
              methods:
                - GET

安装依赖

通过执行yarn add ali-ossnpm install -s ali-oss 安装阿里云 OSS SDK

上传密钥及脚本

上传名「id_rsa」的私钥文件(绑定到 Github 的 SSH 密钥)及名为「my_ssh_executable.sh」的脚本文件到对象存储桶根目录下

# my_ssh_executable.sh
#! /bin/sh
ID_RSA=/tmp/id_rsa
exec /usr/bin/ssh -o StrictHostKeyChecking=no -i $ID_RSA "$@"

image-20210720170214542

修改代码

//index.js
const getRawBody = require('raw-body')
const getFormBody = require('body/form')
const body = require('body')
const execSync = require('child_process').execSync
const oss = require('ali-oss')
const fs = require('fs')
const path = require('path')

exports.handler = async (req, resp, context) => {
  // 设置响应类型
  resp.setHeader('Content-type', 'application/json')

  const client = new oss({
    region: 'oss-cn-shenzhen', // 根据实际设置填写
    accessKeyId: '<Your accessKeyId>',
    accessKeySecret: '<Your accessKeySecret>',
    bucket: 'serverless-cicd',
  })

  async function get(filename, localpath, cmd) {
    try {
      let res = await client.get(filename, localpath)
      if (res.res.status == 200) execSync(`${cmd} ${localpath}`)
    } catch (e) {
      console.error(e)
    }
  }
  // 下载密钥及脚本
  get('id_rsa', '/tmp/id_rsa', 'chmod 0600')
  get('my_ssh_executable.sh', '/tmp/my_ssh_executable.sh', 'chmod +x')

  console.log('下载密钥和脚本完成')

  // 获取webhook提交的推送信息
  getRawBody(req, async function (err, body) {
    body = body.toString().replace('undefined', '",')
    body = JSON.parse(body)
    const ref = body.ref
    const ref_type = body.ref_type
    const repository_name = body.repository.name
    const clone_url = body.repository.clone_url

    if (ref_type != 'tag') {
      resp.send('No tag event')
      return 0
    }

    // 删除可能存在的代码,并重新克隆
    if (fs.existsSync(`/tmp/${repository_name}/`)) execSync(`rm -rf /tmp/${repository_name}`)
    gitclone = `GIT_SSH="/tmp/my_ssh_executable.sh" git clone -b ${ref} ${clone_url} /tmp/${repository_name}`
    try {
      execSync(gitclone)
      console.log('克隆完成')
    } catch (e) {
      resp.send('git clone fail')
    }

    // 执行自定义的打包脚本
    // execSync(`cd /tmp/${repository_name} && sh build.sh`)

    function getFilesList(dir) {
      let res = []
      let files = fs.readdirSync(dir)
      files.forEach((filename) => {
        if (filename[0] == '.') return
        let filepath = path.join(dir, filename)
        let info = fs.statSync(filepath)
        if (info.isFile()) res.push({ filename, filepath })
        else res = res.concat(getFilesList(filepath))
      })
      return res
    }

    // 获取文件列表
    const files = getFilesList(`/tmp/${repository_name}/`)
    // 遍历上传文件
    Promise.all(
      files.map((file) => {
        return new Promise(async (resolve) => {
          let res = await client.put(file.filepath.replace(`/tmp`, ``), file.filepath)
          resolve(`${file.filename} uploading: ${res.res.status == 200}`)
        })
      })
    ).then((r) => {
      console.log(r)
      resp.send(
        JSON.stringify({
          ref,
          ref_type,
          repository_name,
          clone_url,
        })
      )
    })
  })
}

部署

执行s deploy一键部署,通过控制台可以查看到部署结果

image-20210720172223703

配置 GitHub

进入 GitHub 前端仓库 ➡️ Settings ➡️ Webhooks:

  • payload:触发器路径,可复制命令行输出或到控制台查看
  • Content type:application/json
  • events:仅勾选 ☑️ Branch or tag creation

触发测试

cd frontend-project-dir
git tag v1.0 -m 'new version'
git push origin tag v1.0

输出

image-20210720181402727

image-20210720181415414

附录与参考

本文代码

Function Compute 搭建前端 CICD 系统-最佳实践-阿里云 (aliyun.com)

前端项目示例