如何快速開發 Serverless Devs Package ?

Serverless發表於2022-02-11


作者 | 江昱(阿里雲 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 活動、直播,使用者最佳實踐。

相關文章