Serverlessで複数の関数をどう管理できるか検証した時のメモ

## 前提
AWS LambdaでPythonを動かします。個人開発用ではなく、複数人で複数の関数を複数の環境で実行するユースケースを想定しています。

## 調べたこと
* プロジェクトの作成（入門）
* デプロイ
* Pythonパッケージ管理周りの設定
* 環境ごとに設定を管理する方法
* 1つのリポジトリで複数種類のfunctionを管理する時の構成
* その他所感

```
$ python -V
Python 3.8.1

$ serverless -v
Framework Core: 1.82.0
Plugin: 3.8.3
SDK: 2.3.1
Components: 2.34.9
```

## プロジェクトの作成
[公式のQuickStart](https://github.com/serverless/serverless#quick-start)で入門しました。

AWS認証設定のやり方はいくつか方法がありますが、今回の用なユースケースだとserverless.ymlを以下のように設定する形になります。

```serverless.yml
provider:
  name: aws
  runtime: python3.8
  region: ap-northeast-1
  profile: ${self:custom.profiles.${opt:stage, self:provider.stage, 'stg'}}
  stage: ${opt:stage, self:custom.defaultStage}

custom:
  defaultStage: stg
  profiles: # 環境ごとの設定を書く
    stg: [[プロファイル名]]
    prd: [[プロファイル名]]
```

deployコマンド実行時に`--stage stg`のように書くことで、指定した環境の設定が参照されます。プロファイルだけではなく、環境変数なども上記のような書き方で定義します（後述）。

認証設定後の流れは`serverless create`コマンドでプロジェクトを作成して、`serverless deploy`コマンドでデプロイするだけでした。コマンド2つで必要なリソース（Lambdaで使うCloudWatch Logsなども）も作れてHelloWorldできてしまうので0→1で実装したい時はこれほど楽なものはないなと感動しました。

プロジェクトの最小構成は以下のようになります。

```
├── my-service
│   ├── handler.py # Lambdaで実行するファイル
│   └── serverless.yml # それ以外の設定全般を記した設定ファイル
```

## デプロイ

大きく分けて2つデプロイのやり方があるようでした。 `-v` は詳細なログを表示します

### サービス全体のデプロイ

`serverless deploy -v`

一番最初にデプロイする時や、serverless.ymlで管理している設定の変更を反映させる時はこのコマンドを実行します。
初回デプロイの流れとして以下のようなことが行われていました。

* CFnを実行
    * S3バケットとポリシーを作成
* ソースコードをzipで固めてS3にアップロード
* CFnを実行
    * CloudWatch Logs ロググループ作成
    * IAM roleの作成
    * Lambda functionの作成

2回目以降のデプロイでは上記の流れに沿いつつもserverless.ymlファイルで変更があった箇所だけアップロードされていきました。

### 単体のfunctionのデプロイ

`serverless deploy function -f hello -v`

やっていることはローカルでzipファイルを作成し、s3バケットに上げているだけです。
なので変更範囲がソースコードのみの時はこの関数でデプロイする方が圧倒的に早いです。

また前者のデプロイは変更の差分関係なくアップロード処理が走りますが、`deploy function`コマンドの方は差分のチェックを行い、差分がなければdeployパートはskipされるという違いもありました。

## Pythonパッケージ管理周りの設定

外部のパッケージをLambda関数の中で使いたい場合、パッケージのソースコードも一緒にアップロードする必要があります。そのあたりでやらないといけない処理は`serverless-python-packaging`というプラグインを使って行うやり方が紹介されていました。

[How to Handle your Python packaging in Lambda with Serverless plugins | Serverless blog](https://www.serverless.com/blog/serverless-python-packaging)

上記のブログではnumpyを使うソースコードのデプロイのチュートリアルが載っています。

やることとしては、pipで上記のプラグインをインストールして、serverless.ymlにプラグイン用の設定を追記するだけでした。

```serverless.yml
plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: non-linux
    useStaticCache: false
```

`dockerizePip:`では、パッケージのソースコードを落とす際にLambdaのDockerコンテナ(lambci/lambda:build-python3.8)を起動してpip installを行うようにしています。Lambda実行環境でパッケージインストールを行わないと、一部のパッケージが使えないということがあります。

non-linuxをfalseにすると、Docker環境を使わずにパッケージのソースコードを落とすこともできます。

```
# 実行ログの一部
Running docker run --rm -v /Users/-/Desktop/pra_serverless_for_lambda/numpy-test/.serverless/requirements\:/var/task\:z -v /Users/-/Library/Caches/serverless-python-requirements/downloadCacheslspyc\:/var/useDownloadCache\:z -u 0 lambci/lambda\:build-python3.8 python3.8 -m pip install -t /var/task/ -r /var/task/requirements.txt --cache-dir /var/useDownloadCache...
```

### 不具合に気づいた

`serverless-python-requirements`を使うと、 `serverless deploy function`コマンドが使えないという不具合があるみたいです、、

[Unable to deploy single lambda function for version 1\.52\.1 · Issue \#6752 · serverless/serverless · GitHub](https://github.com/serverless/serverless/issues/6752)

### 余談: serverless-python-requirementsチュートリアルではまった話

一番最初にデプロイを行った時に、誤った設定で行なってしまったため(dockerizePipを有効にしていなかった)案の定Lambdaでnumpyが読み込まれませんでした。

dockerizePipの設定を書いた後も改善されなかったのですが、その原因がcacheでした。検証時のserverlessではcacheの読み込みがデフォルトでtrueになっているので、設定ファイルを変更した後も古いパッケージのデータが参照され続けていました。

`useStaticCache: false` でstaticCacheの参照をしないようにすることで解決しました。


## 1つのリポジトリで複数種類のfunctionを管理する時の構成

* function1
  - stg用
  - prd用
* function2
  - stg用
  - prd用

のようなものを同じリポジトリで管理したいケースは、よくあるかと思います。
2パターンやり方が考えられそうでした。

### 案1. functionごとにserverless.ymlファイルを作る

```
├── function1
│   ├── .serverless
│   ├── env_prd.yml
│   ├── env_stg.yml
│   ├── handler.py
│   ├── requirements.txt
│   └── serverless.yml
├── function2
│   ├── .serverless
│   ├── env_prd.yml
│   ├── env_stg.yml
│   ├── handler.py
│   ├── requirements.txt
│   └── serverless.yml
├── .git
├── node_modules
├── package-lock.json
└── package.json
```

### 案2. 1つのserverlss.ymlで全functionを管理する

```
├── function
│   ├── .serverless
│   ├── env_prd.yml
│   ├── env_stg.yml
│   ├── function1.py
│   ├── function2.py
│   ├── requirements.txt
│   └── serverless.yml
├── node_modules
├── package-lock.json
└── package.json
```

案1のメリットとしては、設定ファイルが一つの関数のものだけしか書かれていないから見やすく、デメリットはデプロイを関数ごとに行わなければならなくなります。
案2はその逆で、メリットは1回のデプロイで複数の関数への反映を行えること、デメリットは設定ファイルが混ざるので分かりにくくなることかと思います。

案2に関しては、例えば後からfunctionを追加するようなケースがあった場合、変更のない他の関数でもデプロイが走ってしまいます。`deploy function`コマンドはインフラは作らないので初回デプロイでは使えないため。もちろんソースコードに変更がなければ何回デプロイしようと冪等性は担保されるはずですが...

とはいえそうであればdry-runや、もしくはTerraformみたいに適用前に差分を確認してくれる機能が欲しくなりました。意図しない差分が混じってしまう可能性は考えられるので。

本当に簡単な検証しかしていないので、より設定が複雑になってくるとまた違ったメリデメが出てくるかもしれません。

## 既存リソースからインポートはできるのか

importみたいなコマンドやそれができるpluginを探してみましたが、見つけられませんでした。

最初からServerlessで作る分にはいいけれど、デプロイツールを他のものから乗り換えたいような場合、乗り換えコストはそこそこかかるかもしれません。

## その他所感
* dry-runがなかった
* `SLS_DEBUG=* serverless deploy -v`でより詳細なログを見れる
* ソースコードを上げるS3バケットにライフサイクルポリシーがないのが気になったが、何度かデプロイを繰り返す中でつど5個前のソースコードは削除されていることに気づいた。デフォルトでこの設定がされているなんて気が利いているなと感心した
    * 他にもログの保存期間などもserverless.ymlで設定できるようになっている
* KMSの設定やLambda Layerの設定も行える機能が提供されていて、Lambdaで設定できる一通りのものはServerlessで集約できそうな印象を持った（検証はしていないので使い勝手までは分からない）
* `Serverless remove`コマンドで片付けも簡単
    * ただしこれも同じ設定ファイルで複数関数を管理している場合、関数単位で実行させることはできなかった
* pluginで必要な機能は補っていく思想らしいので、便利なプラグインが色々あるかもしれない

## まとめ

何もない状態から作る分には、爆速でデプロイできる手軽さが良かった👏

Pythonの場合だとserverless-python-requirementsプラグインは欲しいので、function単位でデプロイできないバグがあるのは辛かったです。

また複数ファンクションを管理する時の構成は、他の事例も聞いてみたいなと思いました。（個人的にはfunctionごとにserverless.yml作った方がいいと思うけれど、それが辛いという声もあったので..）