[TECH] 서버 없음のプラグインを 타자 원고で作成する方法 🔨


はじめに
Serverless Framework を使っていて、度々デプロイ時に手動で設定していた作業内容を自動化したいなと思い、プラグイン作成の知識習得も兼ねてライブラリを作成し NPM で公開してみました.
https://www.npmjs.com/package/serverless-amplify-auth
今後も開発する可能性はありそうなので 서버 없음のプラグインを 타자 원고で作成する際の手順をまとめておきました.各手順はザックリと紹介しつつ、主にその過程でハマった点や工夫した点に重きをおいて記事を書いていきます.

動作環境
  • 노드js 12.19.0
  • 서버 프레임워크 없음
  • 프레임 코어: 2.10.0
  • 플러그인: 4.1.1
  • SDK:2.3.2
  • 구성 요소: 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
    
    타자 원고のコンパイル時に必要となる 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 を設定することで、部分的に 타자 원고のコンパイルチェックを外すようにしました.outDir には lib を指定することで、コンパイルされた 타자 원고ファイルは 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 フォルダを指定することで、타자 원고をコンパイルした結果のみがライブラリのソースコードとして取り込まれるようになります.

    서버 없음プラグインの開発を進める
    開発環境が整ったところで早速 서버 플러그인 없음のソースコードを書いていきます.타자 원고のソースコードは 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 として取得できます.
    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.ymlARN を構築する際に利用していて、 index.ts 内で利用しました.
    また、プラグイン内でデプロイ時とは異なる 간략한 상황を使用したいケースもあるかと存じます.それは AWS SDKの SharedIniFileCredentials を用いることで簡易に実装できました.
    注意点として、 SharedIniFileCredentials を用いてプロファイルを切り替える時は、環境変数に AWS_SDK_LOAD_CONFIG="true" を設定する必要がありました.
    設定しないと ConfigError: Missing region in config というエラーが発生してしまい、プロファイルを切り替えることが出来ませんでした.
    それでは、次にプラグインの動作検証用コードを example フォルダに配置していきます.

    서버 없음プラグインの動作検証用プログラムを書く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 内の prestartstart スクリプト実行前に実行されるスクリプトです. npm start を実行すると prestart でプラグインの build タスクを実行した後、 서버 프레임워크 없음のパッケージングを行うことでプラグインの動作確認が行えます.
    今回は 서버 없음の before:package:createDeploymentArtifacts フックを利用しているので、 sls package コマンドで動作検証が可能となっている. before:deploy:deploy 等のデプロイ中に実行されるフックを利用する際は sls deploy --noDeploy コマンド等で動作検証を行う必要がある.
    次に動作検証用の serverless.ymlexample フォルダに配置します.
    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 で用いる検証用の関数を定義します.
    '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
    
    > [email protected] prestart /Users/nika/Desktop/serverless-typescript/example
    > cd ../ && npm run build
    
    
    > [email protected] build /Users/nika/Desktop/serverless-typescript
    > rm -rf lib && tsc
    
    
    > [email protected] 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...
    
    標準出力にあるプラグイン内で出力したログから、適切に値が取得出来ていることが確認出来れば 좋아요.です.

    AWS 소개の切り替えができるか確認してみる
    서버 없음プラグインでの 간략한 상황の切り替えについて、動作検証がまだ出来ていないので確認していきます.serverless.yml 内の custom.serverless-typescript.profile に設定箇所は既に用意してあるので、 ~/.aws/credentials に実在する 간략한 상황名を指定します.
    custom:
        serverless-typescript:
            profile: <プラグイン実行時に使用したい Profile 名>
    
    動作検証のため、 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 を実行して正常に 윤곽が切り替えられていそうか確認してみます.
    # 成功時の実行結果
    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}
    # ...
    
    ちなみに存在しない 간략한 상황を指定した場合の出力は下記のようになります.
    # 失敗時の実行結果
    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}
    # ...
    

    おわりに
    今回初めて 서버 없음プラグインの開発をしてみて、手軽に出来ることが分かったので自動化出来そうな作業は積極的にプラグイン化していきたいなと感じました.
    プラグイン化した後は 지트リポジトリにアップするだけでなく、 NPM のパッケージGitHub Packages として公開しておくと、後々プラグインを利用する際に便利です.また、公開してライブラリのスタッツを見るのは案外楽しく開発のモチベーションにも繋がるのでオススメです.

    参考リンク
  • TypeScript で Serverless Framework の Plugin を書いてみる | Developers.IO
  • typescript 導入した private な npm パッケージの作り方 - 30 歳 SIer から WEB エンジニアで奮闘
  • How To Write Your First Plugin For The Serverless Framework - Part 1
  • 좋은 웹페이지 즐겨찾기