Serverless Framework プラグイン開発
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
event が先にあって、そのイベントに対して hook を仕掛ける、という仕組みで動いているらしい
hook は [time]:[command]:[lifecycle event]
のフォーマットで定義できる。
例)
before:package:compleEvents
after:datadog:clean:init
Datadog のソースを見てみてる限りでは、 command
の部分はコロンで接続した文字列が許容されるように見える。サブコマンドをコロンで表現しているように見える。ぱっと見でわかりづらい。
Command の定義の中でそのコマンドが持つライフサイクルフックを定義してあげて、hooks でコマンド+ライフサイクルイベントに対する実装を紐付けている。
package コマンドもプラグインとして実装されており、ライフサイクルの定義はソースから確認できる。
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',
},
},
});
疑問:
this を bind する書き方が結構紹介されているが、これでは TypeScript の型定義との相性が悪いのではないか??
この記事で解決するか・・・?
アロー関数における this はこのへんとか。
アロー関数において、 this
の解決はレキシカルスコープ
lifecycle hook の実装部分。
serverless-step-functions の実装を参考にしてみると、bluebird promise を使っているっぽいことがわかる。割と
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 を使っている。なのでその流儀に合わせるのが良さそう
※ generateCoreTemplate (lib/plugins/aws/package/lib/generateCoreTemplate.js)
は async function
-
PluginManager.spawn
に関する解説があった- 強制的に別のコマンドを実行させる仕組みらしい
- (本来あったはずの継続処理はそのままやりたい、っていうニーズにはハマらなさそう。その場合はおそらく PluginManager.run かも)
-
SLS_DEBUG
を付けるとコマンドに関するデバッグ情報も出してくれるらしい
余談
プラグイン開発のネタではないが、CLI オプションの validate (この scrap を作った当初やりたかったこと)に関しては自前のリゾルバを作成することで実装可能ということがわかった。
自前のリゾルバを書くことができ、そこでならオプションの無視定時の挙動を 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)}
を使った書き方はあまり好きではない(ユースケースの多くは環境変数で代用できるし、そうすべき、という見解)。
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 では不足なんだろうか?)
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 して公開するならこういうものを利用するのが良いかもしれない
https://www.serverless.com/framework/docs/providers/aws/guide/plugins#provider-specific-plugins
自分が作るとしたら aws provider 限定になるものがほとんどだろうから、勇み足リリースする前にここを押さえておくと良さそう
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 の中でなら参照可能になる