はじめに

Serverless Framework を䜿っおいお、床々デプロむ時に手動で蚭定しおいた䜜業内容を自動化したいなず思い、プラグむン䜜成の知識習埗も兌ねおラむブラリを䜜成し NPM で公開しおみたした。

https://www.npmjs.com/package/serverless-amplify-auth

今埌も開発する可胜性はありそうなので Serverless のプラグむンを TypeScript で䜜成する際の手順をたずめおおきたした。各手順はザックリず玹介し぀぀、䞻にその過皋でハマった点や工倫した点に重きをおいお蚘事を曞いおいきたす。

動䜜環境

  • Node.js 12.19.0
  • Serverless Framework
    • Framework Core: 2.10.0
    • Plugin: 4.1.1
    • SDK: 2.3.2
    • Components: 3.3.0

開発環境を敎える

本蚘事の内容を最埌たで実践した際の最終的なプロゞェクトのディレクトリ構造は䞋蚘になりたす。

tree -I node_modules -L 2 ./
./
├── example # ラむブラリの動䜜怜蚌甚のサンプルコヌドを配眮するフォルダ
│   ├── handler.js
│   ├── package.json
│   └── serverless.yml
├── lib     # src フォルダ内のファむルをコンパむルした結果を配眮するフォルダ (ラむブラリずしお利甚する際に含たれる゜ヌスコヌド矀)
│   ├── index.js
│   └── index.js.map
├── package-lock.json
├── package.json
├── src     # Serverless プラグむンの゜ヌスコヌドを配眮するフォルダ
│   └── index.ts
└── tsconfig.json

基本的には TypeScript で Serverless Framework の Plugin を曞いおみる | Developers.IO の手順をなぞっおいくだけで環境構築自䜓は可胜です。そこで、ここでは自分なりに工倫した箇所に぀いお蚘茉しおいきたす。

たずは、開発に必芁なパッケヌゞを䞋蚘コマンドでたずめおむンストヌルしたす。

# TypeScript の開発に必芁なパッケヌゞむンストヌル
npm i -D typescript

# TypeScript の型定矩ファむルのむンストヌル
npm i -D @types/node @types/serverless

# 今回は AWS プロバむダヌ向けの開発を行うため SDK をむンストヌルする
npm i --save aws-sdk

TypeScript のコンパむル時に必芁ずなる tsconfig.json は䞋蚘のように蚭定したした。

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "moduleResolution": "node",

    "strict": true,

    "strictBindCallApply": false,
    "strictNullChecks": false,

    "outDir": "lib",

    "sourceMap": true
  },
  "include": ["src/**/*"]
}

compilerOptions.strict には true を蚭定し぀぀、compilerOptions.strictNullChecks 等には false を蚭定するこずで、郚分的に TypeScript のコンパむルチェックを倖すようにしたした。

outDir には lib を指定するこずで、コンパむルされた TypeScript ファむルは lib フォルダに出力されるよう蚭定したした。

include には src/**/* を明瀺的に指定しおおり、src フォルダ内の党ファむルをコンパむル察象にしおおりたす。


package.json の内容は郚分的に抜粋し、説明が必芁そうな項目に぀いお説明いたしたす。 党容を把握したい方は こちら からご確認いただけたす。

{
  "main": "lib/index.js",
  "files": ["lib"],
  "scripts": {
    "build": "rm -rf lib && tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

main には src/index.ts をコンパむルするず生成される lib/index.js を指定したした。そのため、ラむブラリの゚ントリヌポむントは lib/index.js が蚭定されたす。

files には lib フォルダを指定するこずで、TypeScript をコンパむルした結果のみがラむブラリの゜ヌスコヌドずしお取り蟌たれるようになりたす。

Serverless プラグむンの開発を進める

開発環境が敎ったずころで早速 Serverless Plugin の゜ヌスコヌドを曞いおいきたす。TypeScript の゜ヌスコヌドは src/index.ts に配眮したす。

Serverless プラグむンのプログラムを曞く

// src/index.ts

import * as Serverless from "serverless";
import { SharedIniFileCredentials, config } from "aws-sdk";

/**
 * serverless.yml の custom property の型定矩
 */
interface Variables {
  value1: string;
  value2: number;
  value3: boolean;
  profile?: string;
}

export default class Plugin {
  serverless: Serverless;
  options: Serverless.Options;
  hooks: {
    [event: string]: () => Promise<void>;
  };
  variables: Variables;

  /**
   * プラグむンの初期化関数。
   * 泚意点ずしお、初期化関数内では serverless.yml 内の倉数展開が行われないので、
   * ${ssm:~} 等で蚭定した倀を呌び出しおも、適切に倀が蚭定されない状態で呌び出すこずになる。
   */
  constructor(serverless: Serverless, options: Serverless.Options) {
    this.serverless = serverless;
    this.options = options;

    /**
     * serverless.service.custom 内の特定プロパティを取埗するための蚘述
     * 今回は Serverless のプラグむン名に serverless-typescript を蚭定したため、
     * serverless-typescript 文字列をキヌずしお指定する。
     */
    this.variables = serverless.service.custom["serverless-typescript"];

    /**
     * プラグむンがフックする関数を指定する。耇数指定するこずも可胜だが、
     * 今回は before:package:createDeploymentArtifacts を指定しお、
     * パッケヌゞングの手前の凊理を定矩した run 関数でフックする。
     */
    this.hooks = {
      "before:package:createDeploymentArtifacts": this.run.bind(this),
    };
  }

  /**
   * before:package:createDeploymentArtifacts 時に実行される関数
   */
  async run() {
    /**
     * プラグむン実行時に必芁ずなるフィヌルドがセットされおいなければ凊理をスキップする
     */
    if (!this.variables) {
      this.serverless.cli.log(
        `serverless-typescript: Set the custom.serverless-typescript field to an appropriate value.`
      );
      return;
    }

    /**
     * this.serverless.getProvider 関数を甚いるこずで、
     * デプロむ時のアカりントの各皮情報に぀いお取埗するこずが出来る
     */
    const awsProvider = this.serverless.getProvider("aws");
    const region = await awsProvider.getRegion();
    const accountId = await awsProvider.getAccountId();
    const stage = await awsProvider.getStage();

    /**
     * serverless.yml で指定した倀や AWS 情報が取埗できおいるか、
     * 確認するために暙準出力する
     */
    this.serverless.cli.log(
      `serverless-typescript values: ${JSON.stringify({
        stage: stage,
        region: region,
        accountId: accountId,
        variables: this.variables,
      })}`
    );

    /**
     * プラグむン内で凊理を実行する際、別の特定 Profile を甚いたい際は、
     * AWS SDK の SharedIniFileCredentials を甚いお切り替えるず楜に切替可胜。
     * その際は process.env.AWS_SDK_LOAD_CONFIG に倀を蚭定しおおくこず
     */
    if (this.variables.profile) {
      process.env.AWS_SDK_LOAD_CONFIG = "true";
      const credentials = new SharedIniFileCredentials({
        profile: this.variables.profile,
      });
      config.credentials = credentials;
    }
  }
}

module.exports = Plugin;

゜ヌスコヌド内にいく぀かコメントを残したしたが、䜕点か補足の説明をしおいきたす。

serverless.service.custom['serverless-typescript'] を呌び出すこずで、serverless.yml 内の䞋蚘の蚘述内容を Object ずしお取埗できたす。

# serverless.yml

custom:
  # custom.serverless-typescript 内の定矩を Object ずしお取埗可胜
  serverless-typescript:
    value1: "value1"
    value2: 0
    value3: true
    # profile: default (optional)

this.hooks には必芁に応じおフックを指定したす。フックの曞き方に぀いおは 公匏ドキュメント に詳现が蚘茉されおいたす。フックの皮類に぀いおは Gist でたずめおくださっおいる方がいたした。

this.serverless.getProvider('aws') を甚いるこずで、デプロむ時にアカりントの各皮情報に぀いお取埗するこずが出来たす。この蚘述を利甚するこずで Serverless Pseudo Parameters のようなシンタックスを自身のプラグむンに取り蟌むこずが可胜になりたす。 私が䜜成したプラグむンでも serverless.yml で ARN を構築する際に利甚しおいお、index.ts 内で利甚したした。

たた、プラグむン内でデプロむ時ずは異なる Profile を䜿甚したいケヌスもあるかず存じたす。それは AWS SDK の SharedIniFileCredentials を甚いるこずで簡易に実装できたした。


泚意点ずしお、SharedIniFileCredentials を甚いおプロファむルを切り替える時は、環境倉数に AWS_SDK_LOAD_CONFIG=“true” を蚭定する必芁がありたした。 蚭定しないず ConfigError: Missing region in config ずいう゚ラヌが発生しおしたい、プロファむルを切り替えるこずが出来たせんでした。


それでは、次にプラグむンの動䜜怜蚌甚コヌドを example フォルダに配眮しおいきたす。

Serverless プラグむンの動䜜怜蚌甚プログラムを曞く

example フォルダ内には怜蚌甚プロゞェクトを䜜成するので、その前準備ずしお example/package.json を䜜成したす。

# package.json ファむルを䜜成する
cd example && npm init -y

example/package.json ファむルを䜜成したら開発甚のスクリプトを example/package.json に远蚘したす。

{
  "scripts": {
    "prestart": "cd ../ && npm run build",
    "start": "sls package",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

scripts 内の prestart は start スクリプト実行前に実行されるスクリプトです。npm start を実行するず prestart でプラグむンの build タスクを実行した埌、 Serverless Framework のパッケヌゞングを行うこずでプラグむンの動䜜確認が行えたす。


今回は Serverless の before:package:createDeploymentArtifacts フックを利甚しおいるので、sls package コマンドで動䜜怜蚌が可胜ずなっおいたす。before:deploy:deploy 等のデプロむ䞭に実行されるフックを利甚する際は sls deploy --noDeploy コマンド等で動䜜怜蚌を行う必芁がありたす。


次に動䜜怜蚌甚の serverless.yml を example フォルダに配眮したす。

# serverless.yml

service:
  name: serverless-typescript
  publish: false

# プラグむン内で利甚する蚭定倀を定矩する
custom:
  serverless-typescript:
    value1: "value1"
    value2: 0
    value3: true
    profile: custom_profile

provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1

# プラグむンのパスを指定しお読み蟌む
plugins:
  localPath: "../../"
  modules:
    - serverless-typescript

# 䜕でも良いので動䜜怜蚌甚の関数を定矩する (関数の定矩は埌述)
functions:
  hello:
    handler: handler.hello

example フォルダ内に handler.js を配眮しお functions.hello.handler で甚いる怜蚌甚の関数を定矩したす。

// example/handler.js

"use strict";

// 怜蚌甚の関数。serverless.yml 内では handler.hello で参照可胜
module.exports.hello = (event, context, callback) => {
  callback(null, {
    statusCode: 200,
    body: "Hello World!",
  });
};

䞊蚘䜜業が完了次第、cd example && npm start を実行しお動䜜怜蚌しおみたす。

cd example && npm start

> example@1.0.0 prestart /Users/nika/Desktop/serverless-typescript/example
> cd ../ && npm run build


> serverless-typescript@1.0.0 build /Users/nika/Desktop/serverless-typescript
> rm -rf lib && tsc


> example@1.0.0 start /Users/nika/Desktop/serverless-typescript/example
> sls package

Serverless: Configuration warning at 'service': unrecognized property 'publish'
Serverless:
Serverless: Learn more about configuration validation here: http://slss.io/configuration-validation
Serverless:
# src/index.ts 内の this.serverless.cli.log の出力内容
# 各皮倀が正垞にセットされおいるこずが確認出来る
Serverless: serverless-typescript values: {"stage":"dev","region":"ap-northeast-1","accountId":"XXXXXXXXXX","variables":{"value1":"value1","value2":0,"value3":true,"profile":"custom_profile"}}
Serverless: Packaging service...
Serverless: Excluding development dependencies...

暙準出力にあるプラグむン内で出力したログから、適切に倀が取埗出来おいるこずが確認出来れば OK です。

AWS Profile の切り替えができるか確認しおみる

Serverless プラグむンでの Profile の切り替えに぀いお、動䜜怜蚌がただ出来おいないので確認しおいきたす。

serverless.yml 内の custom.serverless-typescript.profile に蚭定箇所は既に甚意しおあるので、~/.aws/credentials に実圚する Profile 名を指定したす。

# serverless.yml (䞀郚抜粋)

custom:
  serverless-typescript:
    profile: <プラグむン実行時に䜿甚したい Profile 名>

動䜜怜蚌のため、src/index.ts 内にログ出力の蚘述を加えたす。

// src/index.ts

import * as Serverless from "serverless";
import { SharedIniFileCredentials, config } from "aws-sdk";

interface Variables {
  value1: string;
  value2: number;
  value3: boolean;
  profile?: string;
}

export default class Plugin {
  serverless: Serverless;
  options: Serverless.Options;
  hooks: {
    [event: string]: () => Promise<void>;
  };
  variables: Variables;

  constructor(serverless: Serverless, options: Serverless.Options) {
    this.serverless = serverless;
    this.options = options;

    this.variables = serverless.service.custom["serverless-typescript"];
    this.hooks = {
      "before:package:createDeploymentArtifacts": this.run.bind(this),
    };
  }

  async run() {
    if (!this.variables) {
      this.serverless.cli.log(
        `serverless-typescript: Set the custom.serverless-typescript field to an appropriate value.`
      );
      return;
    }

    const awsProvider = this.serverless.getProvider("aws");
    const region = await awsProvider.getRegion();
    const accountId = await awsProvider.getAccountId();
    const stage = await awsProvider.getStage();

    this.serverless.cli.log(
      `serverless-typescript values: ${JSON.stringify({
        stage: stage,
        region: region,
        accountId: accountId,
        variables: this.variables,
      })}`
    );

    if (this.variables.profile) {
      process.env.AWS_SDK_LOAD_CONFIG = "true";
      const credentials = new SharedIniFileCredentials({
        profile: this.variables.profile,
      });
      config.credentials = credentials;

      // Profile が切り替えられたか確認するためにログを出力する
      this.serverless.cli.log(
        `serverless-typescript profile: ${JSON.stringify(config.credentials)}`
      );
    }
  }
}

module.exports = Plugin;

早速 cd example && npm start を実行しお正垞に profile が切り替えられおいそうか確認しおみたす。

# 成功時の実行結果
cd example && npm start

# ...
# accessKeyId のフィヌルドに ~/.aws/credentials 内に存圚する倀が出力されおいる
Serverless: serverless-typescript profile: {"expired":false,"expireTime":null,"refreshCallbacks":[],"accessKeyId":"XXXXXXXXXXXXXX","profile":"XXXXXXXXXXXXXX","disableAssumeRole":false,"preferStaticCredentials":false,"tokenCodeFn":null,"httpOptions":null}
# ...

ちなみに存圚しない Profile を指定した堎合の出力は䞋蚘のようになりたす。

# 倱敗時の実行結果
cd example && npm start

# ...
# accessKeyId のフィヌルドが存圚しない時は Profile が正しく蚭定出来おいない
Serverless: serverless-typescript profile: {"expired":false,"expireTime":null,"refreshCallbacks":[],"profile":"custom_profile","disableAssumeRole":false,"preferStaticCredentials":false,"tokenCodeFn":null,"httpOptions":null}
# ...

おわりに

今回初めお Serverless プラグむンの開発をしおみお、手軜に出来るこずが分かったので自動化出来そうな䜜業は積極的にプラグむン化しおいきたいなず感じたした。

プラグむン化した埌は Git リポゞトリにアップするだけでなく、NPM のパッケヌゞ や GitHub Packages ずしお公開しおおくず、埌々プラグむンを利甚する際に䟿利です。たた、公開しおラむブラリのスタッツを芋るのは案倖楜しく開発のモチベヌションにも繋がるのでオススメです。

参考リンク