[Python]夜通し環境構築に取り組むもモジュールエラーが解消されなくて廃人になりかけたときの学び

夜通し環境構築に取り組むも、モジュールエラー(ImportError)が解消されなくて廃人になりかけた。エラーの原因はパスが違うという洗礼であったり、使いたいライブラリが使う別のライブラリが存在しなかったり、使っているライブラリが古かったりしたわけだけど、悩んでいるときはそもそもそういう問題が本当に起きているのかどうかすら曖昧なまま試行錯誤していた。このような苦痛をもう二度と味わわないためには結局どうすれば良いのか？ 環境構築の学びをここに記録する。

1. 実行スクリプトはどこのパスにあるか？
2. 実行スクリプトが参照しようとしているライブラリのパスはどこか？
3. そのライブラリに必要なライブラリはあるのか？
4. それらのライブラリのバージョンは正しいか？

なお今回のケースとしては、GCPでJenkinsサーバーを作り、BigQueryを操作するpythonスクリプトをGithubから取得して定期実行することを想定する。

#1. 実行スクリプトはどこのパスにあるか？
エラーが起きているスクリプトがどこで実行されているのかを知ることで、ファイルそのものがその環境で動くかどうかを直接的に確かめることができる。

例えば、ローカルで直接スクリプトを動かすのではなく、GCP(GCE)などのサーバーにインストールしたJenkinsを使うことで、Githubに置いたソースコードを自動的に取得&実行する際にモジュールエラーが起きたとする。
![jenkins_no5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/521810/97d6a789-0f0e-34df-31f8-61e02e85e045.png)


```:Jenkins（コンソール出力）
Started by timer
Running as SYSTEM
Building in workspace /opt/bitnami/jenkins/jenkins_home/workspace/test_py
...
[test_py] $ /bin/bash /tmp/jenkins7138111450797694063.sh
Traceback (most recent call last):
  File "my_test_script.py", line 1, in <module>
    from google.cloud import bigquery
ModuleNotFoundError: No module named 'google.cloud'
Build step 'Execute shell' marked build as failure
Finished: FAILURE
```
Jenkinsは取得してきたスクリプトを置くディレクトリを自動的に作ってくれる。ログを見るとその場所が特定できるので、これを直接動かして検証ができる。

```:CloudShell（ディレクトリの移動とスクリプトの実行）
user:~$ cd /opt/bitnami/jenkins/jenkins_home/workspace/test_py
# Jenkinsがジョブ名に応じて自動的に作成したディレクトリへ移動

user:/opt/bitnami/jenkins/jenkins_home/workspace/test_py$ python my_test_script.py
# Githubから取得したスクリプトの実行
```


#2. 実行スクリプトが参照しようとしているライブラリのパスはどこか？
実際のライブラリを置いているパスと、実行スクリプトがライブラリを参照するために読み込むパスが一致していなければ、ライブラリを読み込むことができずにエラーになる。

例えば、「パスが通っていない」＝環境設定変数のPATHに実際のライブラリが置いてあるパスが設定されていない（ライブラリがインストールされたパスを、スクリプトで読み込めるようになっていない）場合が多い。他には、仮想環境を立ててコマンドを実行している場合にも発生する。具体的には、[venv](https://docs.python.org/ja/3/library/venv.html)などの仮想環境を立ててからスクリプトを実行する場合、使用するライブラリはその仮想環境下からのみ探される。つまり、仮想環境を立てずにインストールしたライブラリは、仮想環境下で実行するスクリプトでは読み込むことはできないし、逆に仮想環境を立ててインストールしたライブラリは、仮想環境を立てずに実行するスクリプトでは使うことができず、モジュールエラーになる。

```:CloudShell（モジュールエラーを起こす誤ったインストール方法の例）
$ cd my_directory
$ pip install --upgrade google-cloud-bigquery
# 仮想環境外にライブラリをインストール

$ python3 -m venv env
$ source env/bin/activate
(env) $ python my_test_script.py
# 仮想環境を有効化して実行

Traceback (most recent call last):
  File "test.py", line 1, in <module>
    from google.cloud import bigquery
ImportError: No module named google.cloud
# モジュールエラーが発生
```

※ちなみに、仮に仮想環境を立てていても、`sudo pip install <ライブラリ名>`で実行してしまうと、ホームディレクトリが仮想環境下ではなく/rootになるため、仮想環境下で実行するスクリプトはモジュールエラーになる。

したがって、

- ライブラリがどのパスにインストールされているのか？
- そのパスはスクリプトで読み込むことができるようになっているのか？

を確認する必要がある。

```:CloudShell（ライブラリインストール時のログから分かるライブラリの存在有無とインストール先のパス）

$ pip install --upgrade google-cloud-bigquery
Requirement already satisfied: google-cloud-bigquery in /usr/local/lib/python3.7/dist-packages (4.56.0)
# 既にインストールされていると言われているが、
# そもそも「/usr/local/lib/python3.7/dist-packages」は
# スクリプトの実行環境(仮想環境)のパスではないのでNG。

$ source env/bin/activate
(env)$ pip install --upgrade google-cloud-bigquery
Requirement already satisfied: proto-plus>=1.10.0 in ./env/lib/python3.7/site-packages (from google-cloud-
bigquery) (1.13.0)
# 既にスクリプトの実行環境(仮想環境下)にインストールされているのでOK。
```

```:CloudShell（手っ取り早いライブラリの確認方法）

(env) account:~$ pip list
Package                  Version  
------------------------ ---------
...
google-api-core          1.25.0   
google-auth              1.24.0   
google-cloud-bigquery    2.6.2    
google-cloud-core        1.5.0    
google-crc32c            1.1.1    
google-resumable-media   1.2.0    
googleapis-common-protos 1.52.0   
...
# 既に「google-cloud-bigquery」がインストールされているのでOK
```
```:CloudShell（手っ取り早いライブラリの確認方法2）
(env) account:~$ pip list
$ pip freeze
...
google-api-core==1.25.0
google-auth==1.24.0
google-cloud-bigquery==2.6.2
google-cloud-core==1.5.0
google-crc32c==1.1.1
google-resumable-media==1.2.0
googleapis-common-protos==1.52.0
...
# 既に（ｒｙ
```

```:CloudShell（PythonPATHから確認できるライブラリのパスと存在有無）
$ pip --version
pip 20.3.3 from /usr/local/lib/python3.7/dist-packages/pip (python 3.7)

$ python
>>> import sys
>>> print(sys.path)
['', '/usr/lib/python2.7', '/usr/lib/python2.7/plat-x86_64-linux-gnu', '/usr/lib/python2.7/lib-tk', '/u
sr/lib/python2.7/lib-old', '/usr/lib/python2.7/lib-dynload', '/usr/local/lib/python2.7/dist-packages', 
'/usr/lib/python2.7/dist-packages']
>>> exit()

# 実際のライブラリの場所と、pythonファイルがライブラリを読み込むときのパスが違う
# そして想定しているメジャーバージョンすら違う
# 以降、仮想環境を立てることで回避


(env) account:~$ python
>>> import sys
>>> print (sys.path)
['', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/home/info/env/lib/python3.7/site-packages']
>>> exit()

(env) account:~$ cd /home/info/env/lib/python3.7/site-packages
(env) account:~/env/lib/python3.7/site-packages$ ls
cachetools                                       google_cloud_bigquery-2.6.2-py3.9-nspkg.pth   pyasn1-0.4.8.dist-info
cachetools-4.2.0.dist-info                       google_cloud_core-1.5.0.dist-info             pyasn1_modules
certifi                                          google_cloud_core-1.5.0-py3.9-nspkg.pth       pyasn1_modules-0.2.8.dist-info
certifi-2020.12.5.dist-info                      google_crc32c                                 __pycache__
cffi                                             google_crc32c-1.1.1.dist-info                 pycparser
cffi-1.14.4.dist-info                            google_crc32c.libs                            pycparser-2.20.dist-info
_cffi_backend.cpython-37m-x86_64-linux-gnu.so    google_resumable_media-1.2.0.dist-info        pytz
cffi.libs                                        google_resumable_media-1.2.0-py3.9-nspkg.pth  pytz-2020.5.dist-info
chardet                                          grpc                                          requests
chardet-4.0.0.dist-info                          grpcio-1.34.1.dist-info                       requests-2.25.1.dist-info
_distutils_hack                                  idna                                          rsa
distutils-precedence.pth                         idna-2.10.dist-info                           rsa-4.7.dist-info
easy_install.py                                  pip                                           setuptools
google                                           pip-18.1.dist-info                            setuptools-51.3.3.dist-info
google_api_core-1.25.0.dist-info                 pkg_resources                                 six-1.15.0.dist-info
google_api_core-1.25.0-py3.9-nspkg.pth           pkg_resources-0.0.0.dist-info                 six.py
googleapis_common_protos-1.52.0.dist-info        proto                                         urllib3
googleapis_common_protos-1.52.0-py3.8-nspkg.pth  protobuf-3.14.0.dist-info                     urllib3-1.26.2.dist-info
google_auth-1.24.0.dist-info                     protobuf-3.14.0-py3.7-nspkg.pth               wheel
google_auth-1.24.0-py3.9-nspkg.pth               proto_plus-1.13.0.egg-info                    wheel-0.36.2.dist-info
google_cloud_bigquery-2.6.2.dist-info            pyasn1

# 読み込み先のパスとライブラリの存在する場所が一致している
```

#3. そのライブラリに必要なライブラリはあるのか？バージョンは正しいか？

pythonファイルが使うライブラリAが、さらに別のライブラリB,Cを使おうとしている場合、BやCが存在しなかったり、バージョンが古かったりすると、ライブラリAがうまくインストールできないことがある。そこで、コマンド`pip install --upgrade <ライブラリ>`を使って関連するライブラリのバージョンを一通り更新すると、うまくインストールできるようになることがある。

```:CloudShell（他のライブラリに依存したエラーと対処）
(env) $ pip install --upgrade google-cloud-bigquery
...
 ----------------------------------------
  Failed building wheel for proto-plus
  Running setup.py clean for proto-plus
  Running setup.py bdist_wheel for grpcio ... error
  Complete output from command /home/info/env/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-kmhkwvwd/grpcio/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" bdist_wheel -d /tmp/pip-wheel-26um0boj --python-tag cp37:
  Found cython-generated files...
  usage: -c [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
     or: -c --help [cmd1 cmd2 ...]
     or: -c --help-commands
     or: -c cmd --help
  
  error: invalid command 'bdist_wheel'
  
  ----------------------------------------
  Failed building wheel for grpcio
  Running setup.py clean for grpcio
Failed to build proto-plus grpcio
Installing collected packages: six, pyasn1, rsa, cachetools, pyasn1-modules, google-auth, protobuf, googleapis-common-protos, pytz, urllib3, chardet, idna, certifi, requests, grpcio, google-api-core, google-cloud-core, proto-plus, pycparser, cffi, google-crc32c, google-resumable-media, google-cloud-bigquery
  Running setup.py install for grpcio ... -

# インストールに失敗。「wheel」というものに関連するものが必要に見える。


(env) $ pip install --upgrade wheel
Collecting wheel
  Downloading https://files.pythonhosted.org/packages/65/63/39d04c74222770ed1589c0eaba06c05891801219272
420b40311cd60c880/wheel-0.36.2-py2.py3-none-any.whl
Installing collected packages: wheel
Successfully installed wheel-0.36.2
# 新しくインストールすることができた

(env) $ pip install --upgrade grpcio
Collecting grpcio
  Using cached https://files.pythonhosted.org/packages/81/5e/168a7fa23a025beed6b7daa0981ace55e394a136db
3082faed7d6cba4556/grpcio-1.34.1.tar.gz
Requirement already satisfied, skipping upgrade: six>=1.5.2 in ./env/lib/python3.7/site-packages (from 
grpcio) (1.15.0)
Building wheels for collected packages: grpcio
  Running setup.py bdist_wheel for grpcio ... -
done
  Stored in directory: /home/info/.cache/pip/wheels/e2/60/7c/617a7c5af21a5d60c41e66ddb55f31235ec7d3f3d2
2d943f51
Successfully built grpcio
Installing collected packages: grpcio
Successfully installed grpcio-1.34.1
...
# 新しくインストールすることができた

(env) $ pip install --upgrade google-cloud-bigquery
Successfully installed cffi-1.14.4 google-api-core-1.25.0 google-cloud-bigquery-2.6.2 google-cloud-core
-1.5.0 google-crc32c-1.1.1 google-resumable-media-1.2.0 proto-plus-1.13.0 pycparser-2.20
# 目的のライブラリをインストールすることができた
```


---
おわり。今後は出力されるログを１つ１つしっかり見ながら、上に挙げたポイントをしらみつぶししていこうと思う。

アドバイスなどがあれば本記事か[Twitterアカウント](https://twitter.com/masatoemata)でお知らせくださいm(__)m