💘

Artifact Registry(Google Cloud)を用いてPythonのプライベートライブラリを管理する

2022/02/25に公開
1

App Engine, Cloud Functions, Cloud Runで共通で利用するPythonのプライベートライブラリを以下ポストで紹介した pip install -t コマンドでコピーする方式をチームで2~3週間運用してみたところ、ライブラリ側のバージョン指定の仕組みがないため、気軽にライブラリ側の更新ができない点に品質コントロールの難しさを感じていました。English version

https://zenn.dev/koshilife/articles/86177b720fd2f5

そんな中、Pythonの依存ライブラリ(Specifying Dependencies)に関するドキュメント(App Engine, Cloud Functions)にArtifact Registryを用いる方式が一番上(推奨位置?)に追記されていたので試してみました。

検証時のソースコード
https://github.com/koshilife/artifact-registry-python-repo-example

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"

https://cloud.google.com/artifact-registry/docs/manage-repos

2. レポジトリにプライベートライブラリを配置する

検証のためfoobarの文字列を返却するだけのパッケージを作ります。

ディレクトリ構成

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末尾の/simplePython Simple Repository APIの都合必要になるそうです。

fooはv1, barはバージョン未指定なのでv2が利用されるのを期待値として依存ライブラリ情報を定義してみました。

https://cloud.google.com/appengine/docs/standard/python3/specifying-dependencies#private_dependencies_with_artifact_registry

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の記載方法と同じです。

https://cloud.google.com/functions/docs/writing/specifying-dependencies-python#private_dependencies_from_artifact_registry

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

guntankobaguntankoba

Cloud Functionの自作モジュール管理のいい方法ないかとこの記事見させていただきました。とても参考になりました。ありがとうございます。