2

如何快速开发 Serverless Devs Package ?

 2 years ago
source link: https://segmentfault.com/a/1190000041358070
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作者 | 江昱(阿里云 Serverless 产品经理)​

Serverless Devs 一直在以开源代码、开放生态的模式进行建设,所以在社区用户参与 Serverless Devs 的建设过程中,就会有两条途径:

1、参与贡献代码:参与代码的贡献相对于来说是有着清晰明确的流程,并且也是参与开源项目常见的途径,Serverless Devs 的贡献者文档,可以参考代码贡献文档;

2、参与贡献 Package:可以开发应用或者组件,发布到 Serverless Registry,以供更多人学习、参考或者使用;这一部分内容可以参考本文;​

Serverless Devs Package 介绍

在说什么是 Serverless Devs Packages 之前,需要先说一下 Serverless Registry,相信很多研发同学都是知道,不同语言/生态都有自己的包管理平台,例如 Python 语言的 Pypi,Node.js 的 NPM。

而所谓的包管理平台,粗暴来说就是管理 “包” 的,这里的 “包”,往往指的是别人已经封装了的某些功能或者能力,我们可以直接使用,而无需我们重复造轮子。

说两个比较形象的例子,如果是搞人工智能,我们不太现实要手动的来写各种算法,往往会通过 Sklearn,Tensorflow 等对应的包来快速的加载某些模型,然后在这个基础上再进步的开发和完善。

而在 Serverless 领域,我们也希望有一个类似的包管理平台,那就是 Serverless Registry:

Serverless ReigstryPython PypiNodejs NPM存储内容Serverless packages(包括 Components 和 Application)Python packagesNodejs packages是否开放标准是是官方源registry.devsapp.cn/simplepypi.python.orgregistry.npmjs.org其它源举例Github registryGitee registry清华源 、豆瓣源tnpm、cnpm是否支持私有化支持支持支持配套工具Serverless Devs 开发者工具Python包管理工具(pip)Node.js打包管理工具(npm)配套命令spipnpm如何使用在s.yaml中直接引用安装之后,在代码中引用安装之后,在代码中引用

与 Python 的 Pypi,Node.js 的 NPM 不同的是,在 Serverless Regsitry 中,Package 是分成两类的,一类是 Component,一类是 Application。

针对 Component 和 Application 的通俗来作区分:

  • Component:指的是组件,类似于一个脚本,通过这个脚本可以做一些事情。例如部署一个函数到某个云平台,调试某个函数,查看某个函数的日志等;
  • Application:指的是应用,类似于一个案例。例如通过某个 Application,可以让用户快速的创建一个 Hello World 的应用,创建一个音视频处理的应用等;
    在 Serverless Devs 的规范中,有关于二者的一个区别图:

而关于 Component 和 Application 的关系是:Application 是一个应用案例的定义,需要通过 Component 进行部署上线。

或许上面的表示有些许的抽象,其实可以用一个形象的案例进行解释。例如:

  • 你通过 Python 的 Tensorflow 框架,做了一个人脸识别的应用,那么此时 Tensorflow 就可以认为是一个 Component,而人脸识别的应用就可以认为是一个 Application;
  • 你通过 Node.js 的 Express、Fs、Path 等依赖,做了一个个人博客,那么此时 Express、Fs、Path 等依赖,就可以认为是不同的 Component,而做出来的这个博客,就可以认为是一个 Application;
  • Serverless Registry Model
  • Serverless Package Model

开发 Package

开发者开发 Serverless Package 的流程相对来说是比较简单的。因为在 Serverless Devs 开发者工具中,已经提供了相对完整的脚手架能力。

开发者只需要执行s init,并且选择Dev Template for Serverless Devs即可:

选择完成,不难发现,此时会让我们继续选择是开发 Component 还是开发 Application:

开发 Component

当选择Component Scaffolding之后,需要给即将开发的 Component 起一个名字(例如deployfunction):

此时,可以根据体统提示,进入到 Component 的项目目录:

此时,可以通过 IDE 打开当前项目,并通过npm进行依赖安装_(因为 Serverless Devs 是基于 Typescript 的项目,所以组件的开发仅支持 Typescript 和 Node.js 语言)_:

此时,可以打开项目中src/index.ts文件,不难发现已经存在一个案例:

import logger from './common/logger';
import { InputProps } from './common/entity';

export default class ComponentDemo {
  /**
   * demo 实例
   * @param inputs
   * @returns
   */
  public async test(inputs: InputProps) {
    logger.debug(input: ${JSON.stringify(inputs.props)});
    logger.info('command test');
    return { hello: 'world' };
  }
}

在该文件中,我们不难发现存在一个test(inputs)方法,该方法是一个打印inputs参数,并返回hello world的案例,但是通过这个简单的案例,我们可以了解到几个事情:

公共方法就是用户可以使用的命令

在项目中,我们可以写多个方法对外暴露,目前只有一个test,但是我们可以增加任何public方法,而这些方法将会成为该组件的命令。例如:

 public async test(inputs: InputProps) {
    logger.debug(input: ${JSON.stringify(inputs.props)});
    logger.info('command test for test');
    return { hello: 'world' };
  }

 public async deploy(inputs: InputProps) {
    logger.debug(input: ${JSON.stringify(inputs.props)});
    logger.info('command test for deploy');
    return { hello: 'world' };
  }​

此时,我们在使用该组件时,组件就具备了两个命令:test 命令和 deploy 命令,为了验证我们的想法,我们可以对项目进行基础的开发态的编译:npm run watch:

此时,我们可以找到 example 目录,进行 deploy 方法的测试,例如:

通过 example 下面的 s.yaml 文件,我们不难看出,这个 yaml 有两个 service_(分别是 component-test 和 component-test2)。_

并且这两个 service,都用了我们同一个组件。所以,在执行s deploy之后获得到预期的结果:即执行了 deploy 方法。

同样的,我们也可以执行 test 命令看一下效果:

要实现的逻辑可以在方法内自由实现

换句话来说,Serverless Devs 工具在加载组件的时候,实际上就是将对应的参数,传递到指定的方法,并且执行该方法。所以,你要实现什么功能都可以写在对应的方法里。

以 Serverless Registry Component 项目为例,我在该项目中,存在一个 Login 的功能,所以我在 Login 中实现了以下内容:

 /**
     * demo 登陆
     * @param inputs
     * @returns
     */
    public async login(inputs: InputProps) {

        const apts = {
            boolean: ['help'],
            alias: {help: 'h'},
        };
        const comParse = commandParse({args: inputs.args}, apts);
        if (comParse.data && comParse.data.help) {
            help([{
                header: 'Login',
                content: Log in to Serverless Registry
            }, {
                header: 'Usage',
                content: $ s cli registry login <options>
            }, {
                header: 'Options',
                optionList: [
                    {
                        name: 'token',
                        description: '[Optional] If you already have a token, you can configure it directly',
                        type: String,
                    }
                ],
            }, {
                header: 'Examples without Yaml',
                content: [
                    '$ s cli registry login',
                    '$ s cli registry login --token my-serverless-registry-token',
                ],
            },]);
            return;
        }
        const tempToken = comParse.data ? comParse.data.token : null
        let st = 0
        let user
        if (tempToken) {
            const fd = await fse.openSync(${getRootHome()}/serverless-devs-platform.dat, 'w+')
            await fse.writeSync(fd, tempToken)
            await fse.closeSync(fd)
            st = 1
        } else {

            const token = random({length: 20})
            const loginUrl = https://github.com/login/oauth/authorize?client_id=beae900546180c7bbdd6&redirect_uri=http://registry.devsapp.cn/user/login/github?token=${token}

            // 输出提醒
            logger.warn("Serverless registry no longer provides independent registration function, but will uniformly adopt GitHub authorized login scheme.")
            logger.info("The system will attempt to automatically open the browser for authorization......")
            try {
                await sleep(2000)
                opn(loginUrl)
            } catch (e) {
                logger.info("Failed to open the default address. Please try to open the following URL manually for authorization: ")
                logger.info(loginUrl)
            }
            await logger.task('Getting', [
                {
                    title: 'Getting login token ...',
                    id: 'get token',
                    task: async () => {
                        for (let i = 0; i < 100; i++) {
                            await sleep(2000)
                            const tempResult = await request('http://registry.devsapp.cn/us...', {
                                params: {
                                    token: token,
                                },
                            })
                            if (!tempResult.Error && tempResult.safety_code) {
                                // 或得到结果, 存储状态
                                const fd = await fse.openSync(${getRootHome()}/serverless-devs-platform.dat, 'w+')
                                await fse.writeSync(fd, tempResult.safety_code)
                                await fse.closeSync(fd)
                                st = 1
                                user = tempResult.login
                                break
                            }
                        }
                    },
                }
            ])
        }
        if (st == 1) {
            logger.log(${user ? user + ': ' : ''}Welcome to Serverless Devs Registry., "green");
        } else {
            logger.error("Login failed. Please log in to GitHub account on the pop-up page and authorize it, or try again later.")
        }
        return null;
    }

在该方法中主要存在几个事情:

  1. 对inputs参数解析,获取用户输入的参数内容;
  2. 如果用户输入的参数带有-h或者--help参数,则输出对应的帮助信息;
  3. 如果用户输入的参数存在--token,则将--token对应的值存入到某个文件中;
  4. 如果用户没有带有--token输入,则直接打开浏览器,访问 Serverless Registry 的登陆地址,并进行相关登陆token的获取;

那么同样的方法,如果是一个部署函数的方法或者命令,我们是不是可以在这个里面实现打包压缩代码,然后调用相关创建函数,更新函数的接口进行函数的创建呢?再比如,想要做一个删除函数的方法,是不是可以在里面调用删除函数的接口呢?

所以可以认为,无论想要实现什么功能,都可以在对应的方法中实现。

关于开发过程中的一些规范

在上面我们说到,Serverless Devs 会带着某些参数调用该方法,那么参数是什么样子的?格式如何,我们该如何解析呢?

再比如,项目最后的 return 有什么用处?如何在项目中获取用户的密钥信息?用户在 Yaml 中写的各种参数如何获取?用户在执行命令时候传递的参数如何获取?

其实这些都可以参考:Serverless Devs Package 的开发规范文档的组件模型代码规范,在这里我们不难发现:

入参 inputs 的结构为:

 {
    "command": "", 
    "project": {
        "projectName": "", 
        "component": "",
        "provider": "",
        "access": ""
    },
    "credentials": {},
    "prop": {},
    "args": "",
    "argsObj": []
}

其中,这些参数的含义:

目录含义command用户所执行的命令project用户的项目基本信息credentials用户的密钥信息prop用户配置的属性/参数args用户传递的参数(字符串形式)argsObj用户传递的参数(解析后的,以数组形式传递)

一个更为具体的例子是,在上面的案例代码中,有一个 test 方法,该方法就是功能实现的方法。此时当用户使用 test 命令时,系统就会携带参数调用该方法。以一个真实案例作为举例说明:

该组件名为hexo,组件核心代码如上所示,具备一个 test 方法,此时用户侧的 Yaml 为:

edition: 1.0.0        #  命令行YAML规范版本,遵循语义化版本(Semantic Versioning)规范
name: FullStack       #  项目名称
access: xxx-account1  #  秘钥别名

services:
  HexoComponent:
    component: hexo
    props:
      region: 'cn-hangzhou'
      codeUri: './src'​

当用户执行s test mytest -a -b abc,此时,组件代码中的test方法,收到的inputs参数实际上是:

{
    "command": "test", 
    "project": {
        "projectName": "HexoComponent", 
        "component": "hexo",
        "provider": "alibaba",
        "access": "release"
    },
    "credentials": {
        "AccountID": "",
        "AccessKeyID": "",
        "AccessKeySecret": ""
    },
    "prop": {
        "Region": "cn-hangzhou",
        "CodeUri": "./src"
    },
    "args": "mytest -a -b abc",
    "argsObj": [
      "mytest", "-a", "-b", "abc"
    ]
}

此时 test 方法会打印日志信息等,并返回最终的结果给命令行工具:{ "hello": "world" }

而关于如何返回帮助文件,如何获取密钥信息,如何解析用户的输入内容,则可以参考 Serverless Devs 提供的 core 包:

在该工具包中,我们可以看到诸多的方法助力我们快速的使用:

例如,获取用户使用密钥,就可以直接引入 core 包,使用对应的getCredential方法即可:

  • 使用方法 1 :不传任何参数的时候,会获取 default 密钥信息

    const { getCredential } = require('@serverless-devs/core');
    async function get() {
      const c = await getCredential();
      console.log('c', c);
    }

  • 使用方法 2 :传参数,获取指定的密钥信息

    const { getCredential } = require('@serverless-devs/core');
    async function get() {
      // 组件接收的inputs
      const inputs = {};
      const c = await getCredential(inputs, 'custom', 'AccountIdByCustom', 'SecretIDByCustom');
      console.log('c', c);
    }

组件的描述

在完成我们的组件功能编写之后,就可以进行组件的描述,所谓的组件的描述,就是要告诉 Serverless Registry,这是一个什么组件,有哪些功能。描述内容在publish.yaml中:

关于该文件的内容以及部分参数的取值,可以参考组件模型元数据。

当然,除了 publish.yaml 之外,在 Serverless Package 的规范中,目录哪还有其他的文件:

|- src # 目录名字可以变更
|   └── 代码目录  
|- package.json: 需要定义好main   
|- publish.yaml: 项目的资源描述   
|- readme.md: 项目简介  
|- version.md: 版本更新内容

目录必须含义src推荐存在统一放置功能实现,当然也可以换成其他的名称,或者平铺到项目下,但是推荐通过src来做统一的存放package.json必须存在Node.js的package.json,需要描述清楚组件的入口文件位置publish.yaml必须存在Serverless Devs Package的开发识别文档readme.md必须存在对该组件的描述,或帮助文档信息version.md推荐存在版本的描述,例如当前版本的更新内容等

最新版本的规范 (0.0.2 版本),将会在近期上线,和 0.0.1 版本不同的是,新版本的 Properties 参数将遵循 JSON Scheme 规范,目前可参考 pr#386)

开发 Application

当选择Application Scaffolding之后,需要给即将开发的 Application 起一个名字(例如helloworld):

Serverless Package 中的 Application 开发相对更为简单,无论是任何编程语言,无论是任何项目,只要可以通过 Serverless Devs 开发者工具进行部署,就可以把它包装成为一个应用。

或者更为准确的来表述,只要你现在有一个可以通过 Serverless Devs 直接部署的项目,那么你就可以:

  1. s init 创建一个应用模板
  2. 把你的那个项目,脱敏后,直接放在src目录下
  3. 对应用进行描述,例如编辑publish.yaml,编辑version.md,编辑readme.md等

这一部分具体情况,可以参考应用模型文档:

特殊格式:在应用模型中,需要存在src/s.yaml文件,作为Serverless Devs识别和使用的资源、行为描述文件,在该文件中,可能涉及到部分内容是需要用户进行填写的,例如用户的密钥名字,用户部署业务的地域等。此时可以参考:

  • "{{ access }}" :直接提醒用户需要输入access这样的一个参数,作为Yaml中所必须的参数;
  • '{{ bucket | alibaba oss bucket }}' ::直接提醒用户需要输入bucket这样的一个参数,作为Yaml中所必须的参数,并以|之后的内容"alibaba oss bucket"作为解释这个参数的含义;
    例如,在某应用的s.yaml中表现为:

edition: 1.0.0
access: "{{ access }}"

services:
  website-starter:
   component: devsapp/website
    actions:
      pre-deploy:
        - run: npm install
          path: ./
        - run: npm run build
          path: ./
    props:
      bucket: '{{ bucket | alibaba oss bucket }}'
      src:
        codeUri: ./
        publishDir: ./build
        index: index.html
      region: cn-hangzhou
      hosts:
        - host: auto

发布 Package

在完成 Serverless Package 的开发之后,为了给更多人使用,还可以将发布到 Registry 中。

发布到 Github/Gitee

关于发布到 Github 或者 Gitee 中,方法很简单:

  1. 创建一个 Repo(代码仓库)
  2. 将整个应用或者组件推到该仓库
  3. 发布一个 Release:此时,在客户端,用户就可以通过s set registry进行registry的切换,来使用相对应的功能了。例如,我在 Github 的账户为 anycodes,我就可以创建一个仓库,名字为 demo,此时,我将我的组件/应用上传到这个仓库中,然后发布一个 Release。此时,我在客户端,将 registry 切换到 Github,然后就可以:
  • 在使用组件的时候,指定组件名为仓库,例如 anycodes/demo
  • 在初始化应用的时候,也可以直接指定应用,例如 anycodes/application

    发布到 Serverless Registry

若想把 Package 发布到 Serverless Registry,可以考虑使用 Serverless Registry Component。

(Serverless Registry 的客户端工具,也是一个组件,所以可以认为 Serverless Registry Component 的这个项目,本身也是当前文章的一个最佳实践。)

在完成组件或者应用的开发流程之后,需要:

  1. 注册并登录 Serverless Registry,实际上就是执行s cli registry login
  2. 进行组件的发布,实际上就是s cli registry publish

当然,Serverless Registry Component 这个项目,除了登陆和发布之外,还有诸多其他的功能,例如:

  • 查看当前登陆账号发布过的 Package
  • 查看某个 Package 的版本信息
  • 查看某个 Package 指定版本信息
  • 删除某个指定版本的 Package
  • 对登陆 token 进行更新

众所周知,一个完整的技术架构的发展,离不开生态社区对他的赋能,无论是 Docker 的 Dockerhub 还是 Python 的 Pypi,再或者是 Node.js 的 NPM,生态的活跃度和我们开发者的幸福感是正相关的。

我们也非常希望 Serverless Devs 可以通过 Serverless Regsitry 这样一个开放的生态,和更多的人一同玩转 Serverless 架构,也期待更多优秀的 Package,被更为广泛的应用。

( END)

更多内容关注 Serverless 微信公众号(ID:serverlessdevs),汇集 Serverless 技术最全内容,定期举办 Serverless 活动、直播,用户最佳实践。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK