Artifact Registry(Google Cloud)を用いてPythonのプライベートライブラリを管理する
App Engine, Cloud Functions, Cloud Runで共通で利用するPythonのプライベートライブラリを以下ポストで紹介した pip install -t
コマンドでコピーする方式をチームで2~3週間運用してみたところ、ライブラリ側のバージョン指定の仕組みがないため、気軽にライブラリ側の更新ができない点に品質コントロールの難しさを感じていました。English version
そんな中、Pythonの依存ライブラリ(Specifying Dependencies)に関するドキュメント(App Engine, Cloud Functions)にArtifact Registryを用いる方式が一番上(推奨位置?)に追記されていたので試してみました。
検証時のソースコード
0. ツール準備
gcloud, python, direnv利用バージョン
$ gcloud app update
$ gcloud --version
Google Cloud SDK 374.0.0
alpha 2022.02.22
beta 2022.02.22
core 2022.02.22
gsutil 5.6
kubectl 1.21.9
$ python3 --version
Python 3.9.10
$ direnv --version
2.28.0
作業ディレクトリ作成, Python仮想環境のセットアップ, パッケージ管理ツール twine
, レポジトリ認証ツール keyrings.google-artifactregistry-auth
導入
$ mkdir hello-artifact-registry
$ cd hello-artifact-registry
$ python3 -m venv venv
$ source venv/bin/activate
$ python3 -m pip install --upgrade pip
$ pip install --upgrade pip setuptools wheel
$ pip install twine keyrings.google-artifactregistry-auth
.envrc
環境変数ファイル
export GCP_PROJECT="<YOUR-GCP-PROJECT>"
# Artifact Registry 読み込み/書き込み権限を持つ サービスアカウントのキーファイルのパス
# 読み込み: CloudRun Dockerfile内で利用
# 書き込み: twine コマンドでのライブラリアップロードで利用
export GOOGLE_APPLICATION_CREDENTIALS="<YOUR-PATH>/google_cloud_credntials.json"
1. Artifact Registryにレポジトリを作る
artifact-example
というPythonライブラリ用のレポジトリを東京リージョン(asia-northeast1
)に作ります。
$ gcloud artifacts repositories create artifact-example \
--repository-format=python \
--location=asia-northeast1 \
--description="Python private libraries"
2. レポジトリにプライベートライブラリを配置する
検証のためfoo
とbar
の文字列を返却するだけのパッケージを作ります。
ディレクトリ構成
python_libs
├── artifact-example-foo
│ ├── artifact_example_foo
│ │ └── __init__.py
│ ├── requirements.txt
│ └── setup.py
└── artifact-example-bar
├── artifact_example_bar
│ └── __init__.py
├── requirements.txt
└── setup.py
artifact-example-foo (v1)
init.py
__version__ = '0.0.1'
def foo():
return 'foo'
artifact-example-bar (v1)
init.py
__version__ = '0.0.1'
def bar():
return 'bar'
wheel形式にパッケージ化した後、作成したArtifact Registryのレポジトリにwhlファイルをアップロード。
$ cd python_libs/artifact-example-foo
$ python setup.py bdist_wheel
$ twine upload --repository-url https://asia-northeast1-python.pkg.dev/${GCP_PROJECT}/artifact-example/ dist/*
$ cd python_libs/artifact-example-bar
$ python setup.py bdist_wheel
$ twine upload --repository-url https://asia-northeast1-python.pkg.dev/${GCP_PROJECT}/artifact-example/ dist/*
ライブラリのバージョン管理/指定の検証ため、
v2として以下のようにファイルを更新して、同様にArtifact Registryのレポジトリにアップロード。
artifact-example-foo (v2)
__version__ = '0.0.2'
def foo():
return 'foo'
def foo2():
return 'foo2'
artifact-example-bar (v2)
__version__ = '0.0.2'
def bar():
return 'bar'
def bar2():
return 'bar2'
(ビルドとアップロードコマンドはほぼ同じなので割愛)
3. クライアントアプリ側でライブラリを利用する
作成したArtifact Registryレポジトリと同一GCPプロジェクトにApp Engine, Cloud Functions, Cloud Runで上記ライブラリを利用したシンプルなアプリを作ってみました。
※ GCPプロジェクトが異なる場合は、クライアントアプリ側のCloud BuildのサービスアカウントにArtifact Registryレポジトリの参照権限付与などの調整が必要みたいです。参考 Doc
App Engine
client_apps/appengine/main.py
from flask import Flask, jsonify
import artifact_example_foo as foo
import artifact_example_bar as bar
app = Flask(__name__)
@app.route('/')
def hello():
return jsonify(dict(result='ok', foo=foo.foo(), foo_version=foo.__version__, bar=bar.bar2(), bar_version=bar.__version__))
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)
client_apps/appengine/requirements.txt
--extra-index-url https://asia-northeast1-python.pkg.dev/<YOUR-GCP-PROJECT>/artifact-example/simple
artifact-example-foo==0.0.1
artifact-example-bar
Flask==2.0.2
1行目の --extra-index-url REPOSITORY_URL
で追加のレポジトリURLを指定している点がポイントです。また、URL末尾の/simple
は Python Simple Repository APIの都合必要になるそうです。
fooはv1, barはバージョン未指定なのでv2が利用されるのを期待値として依存ライブラリ情報を定義してみました。
Cloud Functions
client_apps/functions/main.py
from flask import jsonify
import artifact_example_foo as foo
import artifact_example_bar as bar
def main(request):
return jsonify(dict(result='ok', foo=foo.foo(), foo_version=foo.__version__, bar=bar.bar2(), bar_version=bar.__version__))
client_apps/functions/requirements.txt
--extra-index-url https://asia-northeast1-python.pkg.dev/<YOUR-GCP-PROJECT>/artifact-example/simple
artifact-example-foo==0.0.1
artifact-example-bar
Artifact Registryレポジトリの指定方法はApp Engineの記載方法と同じです。
Cloud Run
client_apps/cloudrun/main.py
import os
from flask import Flask, jsonify
import artifact_example_foo as foo
import artifact_example_bar as bar
app = Flask(__name__)
@app.route("/")
def main():
return jsonify(dict(result='ok', foo=foo.foo(), foo_version=foo.__version__, bar=bar.bar2(), bar_version=bar.__version__))
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
client_apps/cloudrun/requirements.txt
--extra-index-url https://asia-northeast1-python.pkg.dev/<YOUR-GCP-PROJECT>/artifact-example/simple
artifact-example-foo==0.0.1
artifact-example-bar
Flask==2.0.2
gunicorn==20.1.0
こちらもArtifact Registryレポジトリの指定方法は同じです。
client_apps/cloudrun/Dockerfile
# refs: https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-python-service
FROM python:3.10-slim
ENV PYTHONUNBUFFERED True
ENV GOOGLE_APPLICATION_CREDENTIALS "key.json"
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./
RUN pip install --upgrade pip
RUN pip install keyrings.google-artifactregistry-auth
RUN pip install --no-cache-dir -r requirements.txt
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
※ Docker内でpip install
する際にArtifact Registryレポジトリに対する参照権限が必要なため、参照権限を持つサービスアカウントを作り、そのキー情報を
ENV GOOGLE_APPLICATION_CREDENTIALS "key.json"
の1行で指定しています。参考 デプロイスクリプト
クライアントアプリの実行結果
fooは指定したとおりv1, barは指定なしなので最新バージョンv2が利用されていることがわかります。
{
"bar": "bar2",
"bar_version": "0.0.2",
"foo": "foo",
"foo_version": "0.0.1",
"result": "ok"
}
まとめ
Artifact Registryを使うことで、Pythonプライベートライブラリのバージョン管理・指定ができることを確認できました。
現状利用している pip install -t
コマンドでのコピー方式より共通ライブラリの運用管理が改善される気がしています。
おまけ
ライブラリ内でArtifact Registryの特定ライブラリに対する依存関係を持つライブラリの登録・利用についての動作も確認しました。
artifact-example-foobar (v1)
python_libs/artifact-example-foobar/artifact_example_foobar/init.py
import artifact_example_foo as foo
import artifact_example_bar as bar
__version__ = '0.0.1'
def foobar():
return f"{foo.foo()}:{bar.bar()}"
python_libs/artifact-example-foobar/requirements.txt
artifact-example-foo==0.0.1
artifact-example-bar
(ビルドとアップロードコマンドはほぼ同じなので割愛)
pip でartifact-example-foobar
を直接インストールして、Pythonインタプリタで動作を確認しました。
$ pip install --index-url https://asia-northeast1-python.pkg.dev/${GCP_PROJECT}/artifact-example/simple artifact-example-foobar
Looking in indexes: https://asia-northeast1-python.pkg.dev/xxx/artifact-example/simple
Collecting artifact-example-foobar
Downloading https://asia-northeast1-python.pkg.dev/xxx/artifact-example/artifact-example-foobar/artifact_example_foobar-0.0.1-py3-none-any.whl (1.5 kB)
Requirement already satisfied: artifact-example-foo==0.0.1 in _xxx_/hello-artifact-registry/venv/lib/python3.9/site-packages (from artifact-example-foobar) (0.0.1)
Requirement already satisfied: artifact-example-bar in _xxx_/hello-artifact-registry/venv/lib/python3.9/site-packages (from artifact-example-foobar) (0.0.2)
Installing collected packages: artifact-example-foobar
Successfully installed artifact-example-foobar-0.0.1
$ python
>>> import artifact_example_foobar as foobar
>>> foobar.foobar()
'foo:bar'
Discussion
Cloud Functionの自作モジュール管理のいい方法ないかとこの記事見させていただきました。とても参考になりました。ありがとうございます。