Open12

Serverless Framework プラグイン開発

hassaku63hassaku63

Serverless Framework のプラグイン開発について、調べたことをメモしていく。

当面やってみたいのは「deploy または package の前に yaml を解析して何らかのバリデーションを行う機能」なので、関係なさそうなものは drop する

参考記事

Customizing The Serverless Framework With Plugins

ロググループに RetentionInDays を強制的に付与するプラグインを作る。

Serverless Framework における Command, Hooks, Lifecycle Events の概念を学ぶ。

How To Write Your First Plugin For The Serverless Framework - Part 1

プラグイン開発のはじめの一歩

Advanced Plugin Development - Extending The Serverless Core Lifecycle

Serverless Framework が標準的に提供しているライフサイクル・フックを拡張する方法について説明する

※ Serverless 1.x で古い情報かもしれない

What's new for Serverless plugins?

↑の記事の update 版っぽいもの

Sample project

Datadog .. https://github.com/DataDog/serverless-plugin-datadog

hassaku63hassaku63

event が先にあって、そのイベントに対して hook を仕掛ける、という仕組みで動いているらしい

hook は [time]:[command]:[lifecycle event] のフォーマットで定義できる。

例)

  • before:package:compleEvents
  • after:datadog:clean:init

Datadog のソースを見てみてる限りでは、 command の部分はコロンで接続した文字列が許容されるように見える。サブコマンドをコロンで表現しているように見える。ぱっと見でわかりづらい。

https://github.com/DataDog/serverless-plugin-datadog/blob/master/src/index.ts#L35-L62

Command の定義の中でそのコマンドが持つライフサイクルフックを定義してあげて、hooks でコマンド+ライフサイクルイベントに対する実装を紐付けている。

hassaku63hassaku63

package コマンドもプラグインとして実装されており、ライフサイクルの定義はソースから確認できる。

https://github.com/serverless/serverless/blob/master/lib/plugins/package/package.js#L19-L41

    this.commands = {
      package: {
        ...cliCommandsSchema.get('package'),
        lifecycleEvents: [
          'cleanup',
          'initialize',
          'setupProviderConfiguration',
          'createDeploymentArtifacts',
          'compileLayers',
          'compileFunctions',
          'compileEvents',
          'finalize',
        ],
        commands: {
          function: {
            type: 'entrypoint',
            lifecycleEvents: ['package'],
          },
        },
      },
    };

cliCommandsSchema の実装は以下。

// https://github.com/serverless/serverless/blob/master/lib/cli/commands-schema.js#L381-L391
commands.set('package', {
  usage: 'Packages a Serverless service',
  serviceDependencyMode: 'required',
  hasAwsExtension: true,
  options: {
    package: {
      usage: 'Output path for the package',
      shortcut: 'p',
    },
  },
});
hassaku63hassaku63

lifecycle hook の実装部分。

serverless-step-functions の実装を参考にしてみると、bluebird promise を使っているっぽいことがわかる。割と

https://github.com/serverless-operations/serverless-step-functions/blob/fcf0b06eff80f0c7cc5686dc791d426caee45c8c/lib/index.js#L104-L142

    this.hooks = {
      'invoke:stepf:invoke': () => BbPromise.bind(this)
        .then(this.yamlParse)
        .then(this.invoke),
      'package:initialize': () => BbPromise.bind(this)
        .then(this.yamlParse),
     // ...

How To Write Your First Plugin For The Serverless Framework - Part 1 では同期関数だったので、仕様としてはどちらでも受け付けてるように見える。ただし他の実装見てても非同期 (promise or async/await) を使う実装が多そうなのでそっちに寄せるのが良さそう。

今回やりたいのは既存のイベント (after:package:initialize) に独自の hook を追加することであり、serverless 本体のコードも async function を使っている。なのでその流儀に合わせるのが良さそう

https://github.com/serverless/serverless/blob/ff253e32dd5e9c17f46f5a359ebfb9007b6ffa7d/lib/plugins/aws/package/index.js#L58-L59

generateCoreTemplate (lib/plugins/aws/package/lib/generateCoreTemplate.js) は async function

hassaku63hassaku63

https://www.serverless.com/blog/plugin-system-extensions

  • PluginManager.spawn に関する解説があった
    • 強制的に別のコマンドを実行させる仕組みらしい
    • (本来あったはずの継続処理はそのままやりたい、っていうニーズにはハマらなさそう。その場合はおそらく PluginManager.run かも)
  • SLS_DEBUG を付けるとコマンドに関するデバッグ情報も出してくれるらしい
hassaku63hassaku63

余談

プラグイン開発のネタではないが、CLI オプションの validate (この scrap を作った当初やりたかったこと)に関しては自前のリゾルバを作成することで実装可能ということがわかった。

https://serverless.com/framework/docs/providers/aws/guide/variables/#reference-variables-in-javascript-files
自前のリゾルバを書くことができ、そこでならオプションの無視定時の挙動を validate できそう。

# npx sls --version          
Framework Core: 2.31.0 (local)
Plugin: 4.5.2
SDK: 4.2.2
Components: 3.7.6
# serverless.yml
variablesResolutionMode: 20210219

provider:
  name: aws
  runtime: nodejs12.x
  region: ${file(./config.js):config.region}
# config.js
/**
 * https://www.serverless.com/framework/docs/providers/aws/guide/variables/#reference-variables-in-javascript-files
 * 
 * @param {*} serverless 
 * @returns 
 */
module.exports = ({options, resolveConfigurationProperty}) => {
    console.log(options);

    if(!options.region) {
        const errorMessage = 'CLI Option "region" not supplied';
        console.log(errorMessage);
        throw new Error(errorMessage);
    }

    return {
        config: {
            ...options,
        }
    }
};

※とはいえ、普段のプロジェクトでは python を使っているで、このためだけに自前の js を書きたくない気持ちはある。個人的にも ${file(./path/to/module.js)} を使った書き方はあまり好きではない(ユースケースの多くは環境変数で代用できるし、そうすべき、という見解)。

hassaku63hassaku63

https://www.serverless.com/framework/docs/providers/aws/guide/plugins#writing-plugins

sls 2.x 系でプラグイン開発する場合の注意事項。

(プラグイン開発の割と一般的な内容を書いてあるように見えるけど、なんで aws provider の下にあるんだろう...?)

Do not depend on Bluebird API for Promises returned by Framework internals - we are actively migrating away from Bluebird at this point

bluebird の promise に依存してはいけないらしい。

If your plugin adds new properties, ensure to define corresponding schema definitions, please refer to: Extending validation schema

custom, provider など、 serverless.yml の記述にプラグイン独自のパラメータ仕様を追加したいと思ったら、対応する jsonschema を宣言してね、という話らしい。

おそらく VSCode の sls extention の構文サポートを受けられるようになるのではないか?と思っている。もしそうならやったほうがよさそう

Avoid using subcommands as the support for them might become deprecated or removed in next major version of the Framework

subcommands がなんなのかわからないので詳細は不明。プラグインのプロパティに定義する commands のオブジェクト構造で subcommands というキーがあるのか、それともネスト構造が許されないという話なのか。多分前者ではないかという気はしている

Add serverless to peerDependencies in order to ensure officially supported Framework version(s)

依存する sls バージョン指定らしい(package.json では不足なんだろうか?)

hassaku63hassaku63

https://www.serverless.com/framework/docs/providers/aws/guide/plugins#service-local-plugin

plugins:
  localPath: './custom_serverless_plugins'
  modules:
    - custom-serverless-plugin
plugins:
  # This plugin will be loaded from the `.serverless_plugins/` or `node_modules/` directories
  - custom-serverless-plugin
  # This plugin will be loaded from the `sub/directory/` directory
  - ./sub/directory/another-custom-plugin

開発時のテストコードを書く場合に使えそう。 npm link を使っていたが、レポジトリに push して公開するならこういうものを利用するのが良いかもしれない

hassaku63hassaku63

https://www.serverless.com/framework/docs/providers/aws/guide/plugins/#serverless-instance

Note: Variable references in the serverless instance are not resolved before a Plugin's constructor is called, so if you need these, make sure to wait to access those from your hooks.

プラグインのコンストラクタの中では serverless.variables はまだ解決処理されてないよ、という話。

${opt:xxx} とかそのへんの話。hook の中でなら参照可能になる