はじめに

こんにちは、エンジニアの遠藤です。

serverless framework には便利なプラグインがたくさんあり、とても助かります。
ただし、便利なプラグインにもバグがあることはよくあります。
今回はその中でも2つのプラグインのバグを紹介します。

serverless-shell

1つ目は serverless-shell プラグインのバグと回避方法を紹介します。
https://github.com/capitalone/serverless-shell

これは、 serverless framework で設定している環境変数を shell にも適用してくれるライブラリとなっています。

serverless framework で開発をしていると、 Env に依存したコード(例えば、 stage ごとの接続先 URL とか)を書くことがよくあります。
そのため sls invoke local で実行すると動くけど、 $ python handler.py などで実行すると Env の設定が存在せず動かない、と言ったことが起きがちです。

その辺りを解決してくれるプラグインとなっていて、単体テストなどを実行する時にもとても有用です。

例えば

$ sls shell -s local

と実行しておくと、 serverless に定義した local の env を全て環境変数に設定した状態のシェル を起動してくれます。

このプラグイン単体では何の問題もないのですが、別のプラグインと組み合わせると、おかしな挙動になってしまうため、 fork して修正してみました。

バグの内容

バグの内容は、このプラグインを実行して、環境変数を設定すると一部の環境変数に undefined が設定されてしまう、というものでした。
具体的には

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_SESSION_TOKEN
  • AWS_REGION

以上の4つになります。

バグの原因

該当のコードは以下になります。
https://github.com/capitalone/serverless-shell/blob/master/index.js#L110-L119

      const {cachedCredentials} = this.serverless.getProvider('aws');
      if (cachedCredentials) {
        enterpriseCredsEnvVars.AWS_ACCESS_KEY_ID =
          cachedCredentials.accessKeyId;
        enterpriseCredsEnvVars.AWS_SECRET_ACCESS_KEY =
          cachedCredentials.secretAccessKey;
        enterpriseCredsEnvVars.AWS_SESSION_TOKEN =
          cachedCredentials.sessionToken;
        enterpriseCredsEnvVars.AWS_REGION =
          cachedCredentials.region;
      }

cachedCredentials に値が設定されていたら、 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION をセットするという処理になります。
冒頭でも説明しましたが、これだけであれば問題ありません。(ちょっと気にはなるコードですが...)

私が遭遇したケースでは、 serverless-localstack と一緒に使うと、 undefined になる事象が発生してしまいます。
この事象は localstack へのデプロイなどは無関係で、 plugin が install され、 serverless.yml の custom:local フィールドを定義していると発生します。

custom:
  localstack:
    stages:
      - local
    region: ap-northeast-1

コードを少しトレースしてみる

getCredentials() を呼び出す

serverless-localstack では、 awsProvider.getCredentials(); を呼び出しています。
https://github.com/localstack/serverless-localstack/blob/master/src/index.js#L556

{} がセットされる

これを受けて、 awsProvider は cachedCredentials{} をセットしてしまいます。
https://github.com/serverless/serverless/blob/main/lib/plugins/aws/provider.js#L1705-L1710
(python だと {}False なんですけどね...)

なので、この if 文は通らなくない?という気もしますが...
https://github.com/localstack/serverless-localstack/blob/master/src/index.js#L557
それはさておき...

どうすれば回避できた?

元々初期値は null なので
https://github.com/serverless/serverless/blob/main/lib/plugins/aws/provider.js#L1662

serverless-shell のように const {cachedCredentials} = this.serverless.getProvider('aws'); でアクセスしていれば問題なかったようにも見えますが、
null の場合は、強制的に値をセットしに行っているので、意味はなさそうですね。

https://github.com/localstack/serverless-localstack/blob/master/src/index.js#L557

もちろん、他のプラグインから更新される可能性もあるので、 serverless-shell 側でもう少し丁寧に考慮してあげる必要があったかもしれないですね。

修正(fork)したリポジトリ

さて、本来であれば、 https://github.com/capitalone/serverless-shell へ PR を作成したいところなのですが、なんと Public archive となってしまっていました。

なので、使い勝手が悪くなることは承知の上で、渋々ではありますが fork して修正しました。

https://github.com/milldea/serverless-shell

修正コミットは以下です。
純粋にそれぞれの key に値が入っていた場合にのみ、設定するようにしました。
https://github.com/milldea/serverless-shell/commit/d74b5215409b8df1e5adc92a27f7697167a2564b

余談ですが、そもそも

cachedCredentials.accessKeyId
cachedCredentials.secretAccessKey
cachedCredentials.sessionToken
cachedCredentials.region

にどうやって値がセットされるのかはイマイチ良くわかっていません。

当時のコミットログを見ると、 ダッシュボードと連携した時に値が入ってくるようなことを言っているので、アカウントを作って連携すると値が入ってくるのかもしれません。(未検証です)
https://github.com/capitalone/serverless-shell/pull/3

ダッシュボード機能自体は便利そうなので、試してみたい感はありますが、業務データを連携するのは怖いので、いつか個人の AWS アカウントでも作った時に試してみようと思います。

使い方

devDependencies を上書きする方法

まずは、通常通り、 serverless plugin としてインストールをします。

$ serverless plugin install --name serverless-shell

すると package.json には

{
  "devDependencies": {
    "serverless-shell": "^1.1.0"
  },
  "dependencies": {
    "serverless": "^3.28.1"
  }
}

これを、 fork したものに書き換えます。

{
  "devDependencies": {
    "serverless-shell": "https://github.com/milldea/serverless-shell.git"
  },
  "dependencies": {
    "serverless": "^3.28.1"
  }
}

その後、 npm install をしておきます。

カスタムプラグインとして使う

カスタムプラグインとしても使うことができます。

カスタムプラグインの作成方法は以下のブログでも説明していますので、興味のある方はぜひご覧ください。
https://note.milldea.com/posts/serverless-framework-de-fargate-wodepuroisuru-2-2

インストール

# 自身の serverless プロジェクトへ移動
$ cd ./{your project}

# github から clone (ssh or https)
$git clone git@github.com:milldea/serverless-shell.git
$git clone https://github.com/milldea/serverless-shell.git

# serverless-shell 側で必要なモジュールをインストール
$ cd serverless-shell
$ npm i

serverless.yml の設定

provider:
  name: aws
  environment:
    SOME_VAR: foobar
plugins:
  - ./serverless-shell

動作確認

今回は mac の zsh に反映させたかったので、 serverless.yml に以下の設定を追記します。

custom:
  shellBinary: zsh
plugins:
  - serverless-python-requirements
  - ./serverless-shell

また、競合しても問題ないか確認するため、 serverless-localstack も install しておきます。

$ serverless plugin install --name serverless-localstack

実行します。

$ serverless shell -s local
Running "serverless" from node_modules
Using serverless-localstack
Spawning zsh...

確認します。

$ echo $AWS_REGION
us-east-1

良さそうですね。

serverless-dynamodb-local

2つ目は serverless-dynamodb-local プラグインです。
こちらは、 local に dynamodb 環境を構築してくれるプラグインで、 sls offline を実行した時などに、定義に合わせてテーブルなどを自動で作ってくれるものです。

最近は、 local stack を使うことが多く、あまり出番はないのですが、古いプロジェクトではよく使っていました。

バグの内容

serverless-dynamodb-local を使って、 dynamodb local をインストールしようとすると、 403 エラーが発生して正常に終了しなくなる、というものです。

$ sls dynamodb install
Running "serverless" from node_modules
Started downloading dynamodb-local from http://s3-us-west-2.amazonaws.com/dynamodb-local/dynamodb_local_latest.tar.gz into /Users/endo/serverless_dashboard/aws-python/.dynamodb. Process may take few minutes.
✖ Uncaught exception
Environment: darwin, node 16.14.0, framework 3.28.1 (local) 3.28.1v (global), plugin 6.2.3, SDK 4.3.2
Docs:        docs.serverless.com
Support:     forum.serverless.com
Bugs:        github.com/serverless/serverless/issues

Error:
Error: Error getting DynamoDb local latest tar.gz location undefined: 403
...(省略)

バグの原因

こちらは、ちゃんと issues で取り上げられて、修正もされています。
AWS が提供している DynamoDB local へのアクセスに、 http が使えなくなったようで、 sls install を実行すると、 http を使ってアクセスしている URL が 403 を返してしまい、正常に動かなくなってしまったというものです。

https://github.com/99x/serverless-dynamodb-local/issues/294

不具合自体は、 serverless-dynamodb-local ではなく、その依存関係に記載されている dynamodb-localhost 側にあり、こちらでも取り上げられて、修正も済んでいます。

https://github.com/99x/dynamodb-localhost/issues/79

ただし、現時点(2023/3/10 20:30)ではこのバージョンが npm に公開されておらず、使うことができません。

修正(fork)する

これも、あとは作者が公開するだけだと思うので、こちらで PR を作成するものでもなさそうですが、取り急ぎバグを回避したいという理由で、 fork して修正しました。
https://github.com/milldea/serverless-dynamodb-local

修正内容は至ってシンプルで、 dependencies のバージョン部分を git のコミットハッシュに書き換えただけです。
master でも良かったのですが、 master に下位互換のない修正が入った場合にまた謎のバグを踏んでも嫌なので、 明示的に commit 位置を指定しました。
https://github.com/99x/serverless-dynamodb-local/commit/8345c00d719d2bc334259e2960e50055dc99e729

使い方

使い方は、 serverless-shell と同じで、 dependencies の path を変更するか、 カスタムプラグインとしてご利用ください。

動作確認

$ sls dynamodb install                               
Running "serverless" from node_modules
Started downloading dynamodb-local from https://s3-us-west-2.amazonaws.com/dynamodb-local/dynamodb_local_latest.tar.gz into /Users/endo/serverless_dashboard/aws-python/.dynamodb. Process may take few minutes.

 Installation complete!

はい、正常に終わりました。

終わりに

プラグインは便利なものですが、意外な落とし穴があります。
プラグインにもバグがあると知ること、またその中身まで見てみることはとても勉強になるので、そんな点にも注意しながら付き合っていきたいですね。