python3.8でlambda-uploader使用した時にpython3.8が見つからないとエラーを吐かれた時の原因調査


##事の発端
AWSのLambda上にそれなりに複雑になる予定のpythonの関数を追加する事になったが、lambda上ではpipが使用できないため、外部モジュールを使用する関数を使用する場合、
**[一つ一つ使用している外部モジュールを\Python〇〇\Lib\site-packages\] から探し出し纏めてzip化するという賽の河原のようなおちねり作業が発生する。**
当然ながら人間は信用ならない生物のため、こんな作業を毎回手作業でやろうものなら**パンジャンドラムとマーマイトをガンギメした某ブリテンの暴走青列車(ブルートレイン)の如く事故が発生する**　

**事故は起こるさ？　事故は人間が起こすものなんだよ！**
(※加えてこの作業にとても時間が取られる)

そこでlambda-uploaderを使用し、外部モジュールの抽出作業を任せてしまうことで効率化と事故発生率を少しでも下げようと画策した。

##作業環境
- Windows10(64bit)
- python3.8(64bitインストーラ使用)3.8なのは2020/12月末時点でlambdaがpython3.8以降に対応していないから。
- 上記に加えてpythonのインストール箇所は(元来はおすすめされないけど、端末ごと引き継ぐ可能性があるため C:\Program Files以下。本来は各c:\users～以下)


##lambda-uploaderを使用するためには
・lambda.json
　→アップロードするlambda関数の概要を纏めた設定ファイル
・requirements.txt
　→作ったlambda関数が使用する外部モジュールを定義するとこのファイルを参照して自動でピックアップ、zipの際にまとめてくれるみたいです。つまりpip freezeから自動でファイル生成するバッチを作れば一番リスキーな作業を自動化してくれます。
　**事故は起こさせない**

・event.json
　→[テスト時に投げるPOSTリクエストの内容で、GETリクエストのテストの場合は空でも良い](https://colabmix.co.jp/tech-blog/aws-lambda-python-lambda-local/)
　 ということで今回は不要。[他の参考内容](https://blogenist.jp/2019/01/16/7479/) 等でも[記載がない場合](https://runble1.com/python3-aws-lambda-uploder/)が多いので必須ファイルでは無い模様。

・〇〇.py
　→aws lambdaのエントリーポイントである

```python:
def lambda_handler(event, context):～
```

　関数の処理が記載されたpyファイル。



lambda-uploaderの手順としては上記で記載した設定ファイル達を元に、大まかに分けて以下3点を順に行う。
1. build
2. zip
3. upload(aws lambdaに)

jsonファイルに以下の様に設定(セキュリティに関わる情報は伏せてます)して・・・

```json:lambda.json
{
    "name": "testFunction",
    "description": "testFunction description.",
    "region": "ap-northeast-1",
    "handler": "lambda_function.lambda_handler",
    "role": "arn:aws:iam::XXXXXXXXXXXX:role/BlogenistBlogSample",
    "timeout": 30,
    "memory": 128,
    "runtime": "python3.8"   ←明示的に指定しないとデフォルト(時代から捨てられつつあるpython2.7)でアップロードを行う。
}

```

ｲｸｿﾞｰ ﾃﾞｯﾃﾞｯﾃﾞﾃﾞﾃﾞﾃﾞﾝ

## そして(想定外の)事故は起きた
ｶｰﾝ

```
PS C:\workspace(python)\testcode\lambda> lambda-uploader                                                                Building Package
âï¸ Unexpected error. Please report this traceback.
Uploader: 1.3.0
Botocore: 1.16.38
Boto3: 1.19.38

Traceback (most recent call last):
  File "c:\program files\python38\lib\site-packages\lambda_uploader\shell.py", line 194, in main
    _execute(args)
  File "c:\program files\python38\lib\site-packages\lambda_uploader\shell.py", line 83, in _execute
    pkg = package.build_package(pth, requirements,
  File "c:\program files\python38\lib\site-packages\lambda_uploader\package.py", line 51, in build_package
    pkg.build(ignore)
  File "c:\program files\python38\lib\site-packages\lambda_uploader\package.py", line 79, in build
    self.install_dependencies()
  File "c:\program files\python38\lib\site-packages\lambda_uploader\package.py", line 151, in install_dependencies
    self._build_new_virtualenv()
  File "c:\program files\python38\lib\site-packages\lambda_uploader\package.py", line 177, in _build_new_virtualenv
    python_exe = self._python_executable()
  File "c:\program files\python38\lib\site-packages\lambda_uploader\package.py", line 195, in _python_executable
    raise Exception('Unable to locate {} executable'
Exception: Unable to locate python3.8 executable
```

(´・ω・`)

```
Exception: Unable to locate python3.8 executable
```

(´；ω；`)ﾌﾞﾜｯ


最後の行でビルド時にpython3.8が見つからないとエラーを吐瀉(ｵｴ)られる。

##原因を調べてみる
というわけで(最終的な実行環境のpython自体はaws上にあるのでpython.exeいる？とおもいつつ)原因調査開始。

1. PATHが通ってない?
　→問題なし。[\python38\] 及び [\Python38\Lib\site-packages\]両方のPATHを環境変数設定で確認(というかインストール時にPATH通す設定で今まで散々コマンド使ってきているのでこの時点でPATHが原因というのはおかしい)

2. マイナーバージョンも指定しないとだめ？ということでjsonのパラメータを"runtime": "python3.8.6"に修正
　→Exception: Unable to locate python3.8.6 executable
(´・ω・\`)(´・ω・`)

3. いっそのことjsonのruntimeパラメータを削除
　→Exception: Unable to locate python2.7 executable
(´・ω・\`)(´・ω・\`)(´・ω・`) ｿﾘｬｿｳﾀﾞ

3. [lambda-uploader](https://pypi.org/project/lambda-uploader/)モジュールのバグかも知れないので[最新版を見にgitへ](https://github.com/rackerlabs/lambda-uploader)(**pypiに反映せずgit上だけ永遠に更新されているモジュールというのは案外多い**)
　↓
![スクリーンショット 2021-01-07 112928.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75306/7cfed64d-f861-e939-0b18-92e055fbf57d.png)
　↓
![スクリーンショット 2021-01-07 113018222.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75306/df684679-c5ff-0e43-b85e-5ca39569d320.png)

(´；ω；`)ﾌﾞﾜｯ


エラー内容で色々で調べても引っかかって来ない
ということで、Tracebackがあるため、直接lambda-uploaderのソースコードを見に行く。
変に設定をいじっていないなら \Python38\Lib\site-packages\lambda_uploaderに目的のコードが有るためササッとディレクトリ開いて最後に処理が通った箇所付近

```
File "c:\program files\python38\lib\site-packages\lambda_uploader\package.py", line 195, in _python_executable
    raise Exception('Unable to locate {} executable'
```

を調査。

```python:package.py
    def _python_executable(self):
        if self._pyexec is not None:
            python_exe = find_executable(self._pyexec)
            if not python_exe:
195行目→        raise Exception('Unable to locate {} executable'
                                .format(self._pyexec))

```

find_executable関数の処理が怪しいと目星を付け更に呼び出し元を調査。
※vs codeでpython環境建てると複雑なコードでもインテリジェンスが便利なのでおすすめ。いつか開発環境構築を記事にしたい。

```python:spawn.py
def find_executable(executable, path=None):
    """Tries to find 'executable' in the directories listed in 'path'.

    A string listing directories separated by 'os.pathsep'; defaults to
    os.environ['PATH'].  Returns the complete filename or None if not found.
    """
    _, ext = os.path.splitext(executable)
    if (sys.platform == 'win32') and (ext != '.exe'):
        executable = executable + '.exe'

    if os.path.isfile(executable):
        return executable

    if path is None:
        path = os.environ.get('PATH', None)
        if path is None:
            try:
                path = os.confstr("CS_PATH")
            except (AttributeError, ValueError):
                # os.confstr() or CS_PATH is not available
                path = os.defpath
        # bpo-35755: Don't use os.defpath if the PATH environment variable is
        # set to an empty string

    # PATH='' doesn't match, whereas PATH=':' looks in the current directory
    if not path:
        return None

    paths = path.split(os.pathsep)
    for p in paths:
        f = os.path.join(p, executable)
        print(f)
        if os.path.isfile(f):
            print(f)
            # the file exists, we have a shot at spawn working
            return f
    return None

```

上記コードを順番に流し見た限り、環境変数を取得して、分割(そのままだとセミコロンでつながった１行の文字列)、末尾に何かくっつけてファイルチェックしてるなーということで

```python
    for p in paths:
        f = os.path.join(p, executable)
        print(f)     #困った時のprint文
        if os.path.isfile(f):
            # the file exists, we have a shot at spawn working
            return f
```

再び実行。

```
PS C:\workspace(python)\testcode\lambda> lambda-uploader
Building Package
※↓がprintで吐かせた内容
C:\Program Files\Python38\Scripts\python3.8.exe
C:\Program Files\Python38\python3.8.exe

・・・以下他の環境変数の末尾にpython3.8.exeをくっつけた文字列&Traceback
```

上記を見た限りそんな変なことしてないなぁと思いつつ、エラーがpython3.8が見つからないというエラーのため、該当のディレクトリを見に行ったら・・・
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75306/ce58c5a9-da67-90f1-bd57-64ba3aecbb04.png)

(ω・\`) (・ω・\`) (´・ω・\`) (´；ω；`)ﾌﾞﾜｯ

**なんで？**



##それならjsonを書き換える。
ということでそもそも存在しないexeファイルを検索していたというのが原因でした。
そのため、

```json
    "runtime": "python"
```
と書けば**動きはする。ビルドまでは。**
明らかにおかしな動きなため、そのまま動かせばその後で詰まるだろうなと思いながら動かし。当然のようにアップロード処理で落ちました。
例外の内容が

```
botocore.errorfactory.InvalidParameterValueException: An error occurred (InvalidParameterValueException) when calling the UpdateFunctionConfiguration operation: Value python at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [java8, java11, nodejs10.x, nodejs12.x, python2.7, python3.6, python3.7, python3.8, dotnetcore2.1, go1.x, ruby2.5] or be a valid ARN
```
※要約するとlambda.jsonの**runtimeのパラメータにpythonバージョンを書け。**又は本当に有効なARN設定してるの？

とあり、botocoreはawsの認証周りをしてくれる箇所のため、[awsのサービスを操作するパッケージがいるのかな](https://qiita.com/Futo_Horio/items/672eef814be72a278b51)と調べてみたり(結論から言うと見当違いでした)botocoreモジュールにも手を加えてデバッグしたり、そもそもlambda-uploaderモジュールのソースコードが[SyntaxWarning: "is" with a literal. Did you mean "=="?](https://atsuoishimoto.hatenablog.com/entry/2019/09/20/085912)警告を出していて、**やっぱりpython3.8では使えないのでは？**と考えているうちに思いついた下記を試してみました。

![スクリーンショット 2021-01-07 165221.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75306/dad25e9b-0f79-a13f-5755-22b23b8d1eb8.png)

```
PS C:\workspace(python)\testcode\lambda> lambda-uploader                                                                                                                                     Building Package
Uploading Package
Fin
```

aws lambda上を確認しても正常にアップロード出来ていました。
ものすごくスッキリしない解決になってしまい申し訳ありません。

しかし、python〇〇.exeなんて一体どういう流れで入ってくるのだろうか・・・？(vitualenvが先に割り込んで生成されたpython〇〇.exeを参照するのかと思ってたが中身を見てもvirtualenvより先に参照していた。)

誰か原因をご存知ならコメントでご指摘下さい。

##結論
**pythonの素晴らしいところはPIPしてきたモジュールを直接書き換え、デバッグして動作確認できるところ。(終わったらもとに戻すことを忘れずに)**

また、lambda-uploaderは３年近く更新がなく、既存のコードも最近のpythonの仕様変更によりトラブルを抱え始めているため、今後は使えなくなる事を想定したほうが良いかもしれません。