Python で Cloud Workstations を使用したインナーループ開発

1. 概要

このラボでは、コンテナ化された環境で Python アプリケーションを開発するソフトウェア エンジニア向けに、開発ワークフローを効率化するための特長と機能を紹介します。一般的なコンテナ開発では、ユーザーがコンテナの詳細とコンテナのビルドプロセスを理解する必要があります。さらに、デベロッパーは通常、作業の中断を余儀なくされ、IDE から離れてリモート環境でアプリケーションのテストやデバッグを行う必要もあります。このチュートリアルで説明するツールとテクノロジーを使用すると、デベロッパーは IDE を離れることなく、コンテナ化されたアプリケーションを効果的に操作できます。

学習内容

このラボでは、GCP でコンテナを使用して開発するための次のような方法について学びます。

  • 新しい Python スターター アプリケーションを作成する
  • 開発プロセスを確認する
  • シンプルな CRUD REST サービスを開発する
  • GKE へのデプロイ
  • エラー状態のデバッグ
  • ブレークポイント / ログの利用
  • 変更を GKE にホット デプロイして戻す

58a4cdd3ed7a123a.png

2. 設定と要件

セルフペース型の環境設定

  1. Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列です。この値はいつでも更新できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud コンソールでは一意の文字列が自動生成されます。通常、それが何であるかは関係ありません。ほとんどの Codelab では、プロジェクト ID を参照する必要があります(通常は PROJECT_ID として識別されます)。生成された ID が気に入らない場合は、別のランダムな ID を生成できます。または、ご自身でお試しになることもできます。このステップを終えた後は変更できず、プロジェクト期間中は維持されます。
  • なお、3 つ目の値は、一部の API で使用される [プロジェクト番号] です。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud コンソールで課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルの終了後に課金されないようにリソースをシャットダウンするには、作成したリソースを削除するか、プロジェクト全体を削除します。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloudshell エディタを起動する

このラボは、Google Cloud Shell エディタで使用するように設計、テストされています。エディタにアクセスするには、

  1. https://console.cloud.google.com から Google プロジェクトにアクセスします。
  2. 右上にある Cloud Shell エディタのアイコンをクリックします。

8560cc8d45e8c112.png

  1. ウィンドウの下部に新しいペインが開きます
  2. [エディタを開く] ボタンをクリックします。

9e504cb98a6a8005.png

  1. エディタが開き、右側にエクスプローラ、中央部分にエディタが表示されます
  2. 画面の下部にターミナル ペインも表示されます。
  3. ターミナルが開いていない場合は、`Ctrl+` キーの組み合わせを使用して新しいターミナル ウィンドウを開きます。

環境設定

Cloud Shell で、プロジェクトのプロジェクト ID とプロジェクト番号を設定します。これらを PROJECT_ID 変数と PROJECT_ID 変数として保存します。

export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID \
    --format='value(projectNumber)')

このラボで使用するインフラストラクチャをプロビジョニングする

このラボでは、GKE にコードをデプロイし、Spanner データベースに保存されているデータにアクセスします。また、IDE としてクラウド ワークステーションも使用します。以下の設定スクリプトは、このインフラストラクチャを準備します。

  1. 設定スクリプトをダウンロードして、実行可能にします。
wget https://raw.githubusercontent.com/GoogleCloudPlatform/container-developer-workshop/main/labs/python/setup_with_cw.sh
chmod +x setup_with_cw.sh
  1. setup_with_cw.sh ファイルを開き、現在 CHANGEME に設定されているパスワードの値を編集します
  2. 設定スクリプトを実行して、このラボで使用する GKE クラスタと Spanner データベースを立ち上げます。
./setup_with_cw.sh &

Cloud Workstations クラスタ

  1. Cloud コンソールで Cloud Workstations を開きます。クラスタが READY ステータスになるまで待ちます。

305e1a3d63ac7ff6.png

ワークステーション構成の作成

  1. Cloud Shell セッションが切断された場合は、[再接続] をクリックします。gcloud cli コマンドを実行してプロジェクト ID を設定しますコマンドを実行する前に、以下のサンプル プロジェクト ID を Qwiklabs プロジェクト ID に置き換えてください。
gcloud config set project qwiklabs-gcp-project-id
  1. 以下のスクリプトをダウンロードしてターミナルで実行し、Cloud Workstations 構成を作成します。
wget https://raw.githubusercontent.com/GoogleCloudPlatform/container-developer-workshop/main/labs/python/workstation_config_setup.sh
chmod +x workstation_config_setup.sh
./workstation_config_setup.sh
  1. [構成] セクションで結果を確認します。READY ステータスに移行するまでに 2 分かかります。

2e23c2e9983d1ccf.png

  1. コンソールで Cloud Workstations を開き、新しいインスタンスを作成します。

a53adeeac81a78c8.png

  1. 名前を my-workstation に変更し、既存の構成(codeoss-python)を選択します。

f052cd47701ec774.png

  1. [ワークステーション] セクションで結果を確認します。

ワークステーションを起動

  1. ワークステーションを起動して起動します。ワークステーションの起動には数分かかります。

682f8a307032cba3.png

  1. アドレスバーのアイコンをクリックして、サードパーティ Cookie を許可します。1b8923e2943f9bc4.png

fcf9405b6957b7d7.png

  1. [サイトが動作していない場合] をクリックします。

36a84c0e2e3b85b.png

  1. [Cookie を許可] をクリックします。

2259694328628fba.png

  1. ワークステーションを起動すると、Code OSS IDE が表示されます。[完了マークを付ける] をクリックしますワークステーションの IDE では

94874fba9b74cc22.png

3. 新しい Python スターター アプリケーションを作成する

このセクションでは、新しい Python アプリケーションを作成します。

  1. 新しいターミナルを開きます。

c31d48f2e4938c38.png

  1. 新しいディレクトリを作成し、ワークスペースとして開きます。
mkdir music-service && cd music-service

code-oss-cloud-workstations -r --folder-uri="$PWD"

このメッセージが表示された場合は [Allow] ボタンをクリックし、ワークステーションにコピーして貼り付けられるようにします。

58149777e5cc350a.png

  1. requirements.txt というファイルを作成し、次の内容をコピーします。

789e8389170bd900.png

Flask
gunicorn
google-cloud-spanner
ptvsd==4.3.2
  1. app.py という名前のファイルを作成し、次のコードを貼り付けます。
import os
from flask import Flask, request, jsonify
from google.cloud import spanner

app = Flask(__name__)

@app.route("/")
def hello_world():
    message="Hello, World!"
    return message

if __name__ == '__main__':
    server_port = os.environ.get('PORT', '8080')
    app.run(debug=False, port=server_port, host='0.0.0.0')

  1. Dockerfile という名前のファイルを作成し、次のコードを貼り付けます。
FROM python:3.8
ARG FLASK_DEBUG=0
ENV FLASK_DEBUG=$FLASK_DEBUG
ENV FLASK_APP=app.py
WORKDIR /app
COPY requirements.txt .
RUN pip install --trusted-host pypi.python.org -r requirements.txt
COPY . .
ENTRYPOINT ["python3", "-m", "flask", "run", "--port=8080", "--host=0.0.0.0"]

: FLASK_DEBUG=1 を使用すると、コードの変更を Python Flask アプリに自動的に再読み込みできます。この Dockerfile を使用すると、この値をビルド引数として渡すことができます。

マニフェストを生成する

ターミナルで次のコマンドを実行して、デフォルトの skaffold.yaml と deployment.yaml を生成します。

  1. 次のコマンドを使用して Skaffold を初期化する
skaffold init --generate-manifests

プロンプトが表示されたら、矢印キーでカーソルを動かし、Space キーを使用してオプションを選択します。

次のいずれかを選択:

  • ポートに対する 8080
  • y: 構成を保存する

Skaffold 構成を更新する

  • デフォルトのアプリケーション名を変更する
  • skaffold.yaml を開きます。
  • 現在 dockerfile-image として設定されているイメージ名を選択します
  • 右クリックして [すべてのオカレンスを変更] を選択します。
  • 新しい名前として「python-app」と入力します。
  • build セクションをさらに編集し、
  • docker.buildArgs を追加して FLASK_DEBUG=1 を渡す
  • 設定を同期して、IDE から実行中のコンテナに *.py ファイルへの変更を読み込みます

編集後、skaffold.yaml ファイルのビルド セクションは次のようになります。

build:
 artifacts:
 - image: python-app
   docker:
     buildArgs:
       FLASK_DEBUG: "1"
     dockerfile: Dockerfile
   sync:
     infer:
     - '**/*.py'

Kubernetes 構成ファイルを変更する

  1. デフォルトの名前を変更する
  • deployment.yaml ファイルを開く
  • 現在 dockerfile-image として設定されているイメージ名を選択します
  • 右クリックして [すべてのオカレンスを変更] を選択します。
  • 新しい名前として「python-app」と入力します。

4. 開発プロセスのチュートリアル

これで、ビジネス ロジックが追加されたので、アプリケーションのデプロイとテストができるようになりました。次のセクションでは、Cloud Code プラグインの使用方法を紹介します。特に、このプラグインは skaffold と統合され、開発プロセスを効率化できます。次の手順で GKE にデプロイすると、Cloud Code と Skaffold がコンテナ イメージを自動的にビルドして Container Registry に push し、your アプリケーションを GKE にデプロイします。この処理はバックグラウンドで行われ、デベロッパー フローでは詳細が抽象化されます。

Google Cloud にログインする

  1. Cloud Code アイコンをクリックし、[Sign in to Google Cloud] を選択します。

1769afd39be372ff.png

  1. [ログインに進む] をクリックします。

923bb1c8f63160f9.png

  1. ターミナルで出力を確認し、リンクを開きます。

517fdd579c34aa21.png

  1. Qwiklabs の受講者用認証情報でログインします。

db99b345f7a8e72c.png

  1. [許可] を選択:

a5376553c430ac84.png

  1. 確認コードをコピーして、[ワークステーション] タブに戻ります。

6719421277b92eac.png

  1. 確認コードを貼り付けて、Enter キーを押します。

e9847cfe3fa8a2ce.png

Kubernetes クラスタを追加

  1. クラスタを追加する

62a3b97bdbb427e5.png

  1. Google Kubernetes Engine を選択します。

9577de423568bbaa.png

  1. プロジェクトを選択します。

c5202fcbeebcd41c.png

  1. [python-cluster] を選択します。初期セットアップで作成された すべてのジョブに適用されます

719c2fc0a7f9e84f.png

  1. Cloud Code の Kubernetes クラスタリストにクラスタが表示されます。ここからクラスタを操作、探索します。

7e5f50662d4eea3c.png

gcloud CLI を使用して現在のプロジェクト ID を設定する

  1. Qwiklabs ページからこのラボのプロジェクト ID をコピーします。

fcff2d10007ec5bc.png

  1. ターミナルで gcloud cli コマンドを実行してプロジェクト ID を設定します。コマンドを実行する前に、サンプル プロジェクト ID を置き換えます。プロジェクト ID を置き換えてから、以下のコマンドを実行します。
gcloud config set project qwiklabs-gcp-project-id

Kubernetes へのデプロイ

  1. Cloud Shell エディタの下部にあるペインで、[Cloud Code] を選択します。

d99a88992e15fea9.png

  1. 上部に表示されるパネルで、[Run on Kubernetes] を選択します。プロンプトが表示されたら、[はい] を選択して現在の Kubernetes コンテキストを使用します。

bfd65e9df6d4a6cb.png

このコマンドは、ソースコードのビルドを開始し、テストを実行します。ビルドとテストの実行には数分かかります。これらのテストには、単体テストと、デプロイ環境用に設定されているルールをチェックする検証ステップが含まれます。この検証ステップはすでに構成されているため、開発環境で作業している場合でも、デプロイの問題に関する警告が表示されます。

  1. コマンドを初めて実行すると、画面の上部に現在の Kubernetes コンテキストが必要かどうかを尋ねるプロンプトが表示されます。[はい] を選択します。現在のコンテキストを受け入れて使用します。
  2. 次に、使用する Container Registry を尋ねるプロンプトが表示されます。Enter キーを押して、指定されたデフォルト値を受け入れます。
  3. [出力] を選択します。Tab キーを押して進行状況と通知を表示します。プルダウンで [Kubernetes: Run/Debug] を選択します。

9c87ccbf5d06f50a.png

  1. [Kubernetes: Run/Debug - Detailed] を選択します。右側のチャネル プルダウンから、追加の詳細情報や、コンテナからライブ ストリーミングされるログを確認できます。

804abc8833ffd571.png

ビルドとテストが完了すると、[出力] タブのログの [Kubernetes: Run/Debug] に URL http://localhost: 8080 が表示されます。表示されます。

  1. Cloud Code ターミナルで、出力の最初の URL(http://localhost:8080)にカーソルを合わせ、表示されたツールチップで [Open Web Preview] を選択します。
  2. 新しいブラウザタブが開き、「Hello, World!」というメッセージが表示されます

ホットリロード

  1. app.py ファイルを開きます。
  2. 応答メッセージを「Hello from Python」に変更します

すぐに、Output ウィンドウの Kubernetes: Run/Debug ビューで、ウォッチャーが更新されたファイルを Kubernetes のコンテナと同期します。

Update initiated
Build started for artifact python-app
Build completed for artifact python-app

Deploy started
Deploy completed

Status check started
Resource pod/python-app-6f646ffcbb-tn7qd status updated to In Progress
Resource deployment/python-app status updated to In Progress
Resource deployment/python-app status completed successfully
Status check succeeded
...
  1. Kubernetes: Run/Debug - Detailed ビューに切り替えると、ファイルの変更を認識し、アプリをビルドして再デプロイします。
files modified: [app.py]
Syncing 1 files for gcr.io/veer-pylab-01/python-app:3c04f58-dirty@sha256:a42ca7250851c2f2570ff05209f108c5491d13d2b453bb9608c7b4af511109bd
Copying files:map[app.py:[/app/app.py]]togcr.io/veer-pylab-01/python-app:3c04f58-dirty@sha256:a42ca7250851c2f2570ff05209f108c5491d13d2b453bb9608c7b4af511109bd
Watching for changes...
[python-app] * Detected change in '/app/app.py', reloading
[python-app] * Restarting with stat
[python-app] * Debugger is active!
[python-app] * Debugger PIN: 744-729-662
  1. 前に表示したブラウザタブを更新して、更新された結果を確認します。

デバッグ

  1. デバッグビューに移動し、現在のスレッド 647213126d7a4c7b.png を停止します。必要に応じて、実行後にクリーンアップすることもできます。
  2. 70d6bd947d04d1e6.png
  3. 下部のメニューで Cloud Code をクリックし、Debug on Kubernetes を選択して、アプリを debug モードで実行します。
  • Output ウィンドウの Kubernetes Run/Debug - Detailed ビューで、skaffold がこのアプリケーションをデバッグモードでデプロイします。
  1. プロセスが完了したとき。デバッガが接続され、[出力] タブに Attached debugger to container "python-app-8476f4bbc-h6dsl" successfully. と表示され、URL http://localhost:8080 が表示されています。
Port forwarding pod/python-app-8bd64cf8b-cskfl in namespace default, remote port 5678 -> http://127.0.0.1:5678
  1. 下部のステータスバーの色が青からオレンジ色に変わり、デバッグモードになっていることを示します。
  2. Kubernetes Run/Debug ビューで、Debuggable コンテナが開始されていることを確認します。
**************URLs*****************
Forwarded URL from service python-app: http://localhost:8080
Debuggable container started pod/python-app-8bd64cf8b-cskfl:python-app (default)
Update succeeded
***********************************

ブレークポイントの活用

  1. app.py ファイルを開きます。
  2. return message」というステートメントを見つけます。
  3. 行番号の左側にある空白スペースをクリックして、その行にブレークポイントを追加します。ブレークポイントが設定されたことを示す赤いインジケーターが表示される
  4. 初回実行時には、ソースがコンテナ内のどこにあるかを尋ねるプロンプトが表示されます。この値は、Dockerfile 内のディレクトリに関連付けられています。

Enter キーを押して、デフォルトを受け入れます。

fccc866f32b5ed86.png

アプリケーションのビルドとデプロイには数分かかります。

  1. ブラウザを再読み込みすると、デバッガがブレークポイントでプロセスを停止し、GKE でリモートで実行されているアプリケーションの変数と状態を調査できるようになります。
  2. クリックして [変数] セクションまで移動します。
  3. [Locals] をクリックして、"message" 変数を見つけます。
  4. 変数名「message」をダブルクリックします。ポップアップで、値を "Greetings from Python" など別のものに変更します。
  5. デバッグ用コントロール パネルの [続行] ボタンをクリックします。607c33934f8d6b39.png
  6. ブラウザでレスポンスを確認します。入力した値が更新されています。
  7. 「Debug」と表示モードに切り替えるには停止ボタン 647213126d7a4c7b.png を押します。ブレークポイントをもう一度クリックすると、ブレークポイントを削除できます。

5. シンプルな CRUD REST サービスの開発

これで、アプリケーションはコンテナ化された開発用に完全に構成され、Cloud Code での基本的な開発ワークフローはひととおり確認できました。以降のセクションでは、Google Cloud のマネージド データベースに接続する REST サービス エンドポイントを追加して、学習した内容を実践します。

REST サービスをコーディングする

以下のコードでは、アプリケーションの基盤となるデータベースとして Spanner を使用するシンプルな REST サービスを作成します。次のコードをアプリケーションにコピーしてアプリケーションを作成します。

  1. app.py を次の内容に置き換えて、メイン アプリケーションを作成します。
import os
from flask import Flask, request, jsonify
from google.cloud import spanner


app = Flask(__name__)


instance_id = "music-catalog"

database_id = "musicians"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)


@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

@app.route('/singer', methods=['POST'])
def create():
    try:
        request_json = request.get_json()
        singer_id = request_json['singer_id']
        first_name = request_json['first_name']
        last_name = request_json['last_name']
        def insert_singers(transaction):
            row_ct = transaction.execute_update(
                f"INSERT Singers (SingerId, FirstName, LastName) VALUES" \
                f"({singer_id}, '{first_name}', '{last_name}')"
            )
            print("{} record(s) inserted.".format(row_ct))

        database.run_in_transaction(insert_singers)

        return {"Success": True}, 200
    except Exception as e:
        return e



@app.route('/singer', methods=['GET'])
def get_singer():

    try:
        singer_id = request.args.get('singer_id')
        def get_singer():
            first_name = ''
            last_name = ''
            with database.snapshot() as snapshot:
                results = snapshot.execute_sql(
                    f"SELECT SingerId, FirstName, LastName FROM Singers " \
                    f"where SingerId = {singer_id}",
                    )
                for row in results:
                    first_name = row[1]
                    last_name = row[2]
                return (first_name,last_name )
        first_name, last_name = get_singer()  
        return {"first_name": first_name, "last_name": last_name }, 200
    except Exception as e:
        return e


@app.route('/singer', methods=['PUT'])
def update_singer_first_name():
    try:
        singer_id = request.args.get('singer_id')
        request_json = request.get_json()
        first_name = request_json['first_name']
        
        def update_singer(transaction):
            row_ct = transaction.execute_update(
                f"UPDATE Singers SET FirstName = '{first_name}' WHERE SingerId = {singer_id}"
            )

            print("{} record(s) updated.".format(row_ct))

        database.run_in_transaction(update_singer)
        return {"Success": True}, 200
    except Exception as e:
        return e


@app.route('/singer', methods=['DELETE'])
def delete_singer():
    try:
        singer_id = request.args.get('singer')
    
        def delete_singer(transaction):
            row_ct = transaction.execute_update(
                f"DELETE FROM Singers WHERE SingerId = {singer_id}"
            )
            print("{} record(s) deleted.".format(row_ct))

        database.run_in_transaction(delete_singer)
        return {"Success": True}, 200
    except Exception as e:
        return e

port = int(os.environ.get('PORT', 8080))
if __name__ == '__main__':
    app.run(threaded=True, host='0.0.0.0', port=port)

データベース構成の追加

Spanner に安全に接続するには、ワークロード ID を使用するようにアプリケーションを設定します。これにより、アプリケーションは独自のサービス アカウントとして機能し、データベースにアクセスする際の個別の権限を持ちます。

  1. deployment.yaml を更新します。ファイルの末尾に次のコードを追加します(以下の例ではタブのインデントを維持してください)。
      serviceAccountName: python-ksa
      nodeSelector:
        iam.gke.io/gke-metadata-server-enabled: "true" 

変更後の仕様セクションは次のようになります。

   spec:
     containers:
     - name: python-app
       image: python-app
     serviceAccountName: python-ksa
     nodeSelector:
       iam.gke.io/gke-metadata-server-enabled: "true"

アプリケーションのデプロイと検証

  1. Cloud Shell エディタの下部にあるペインで Cloud Code を選択し、画面上部の Debug on Kubernetes を選択します。
  2. ビルドとテストが完了すると、[Output] タブに Resource deployment/python-app status completed successfully と表示され、URL として「Forwarded URL from service python-app: http://localhost:8080」が表示されます。
  3. いくつかのエントリを追加します。

cloudshell ターミナルから、次のコマンドを実行します。

curl -X POST http://localhost:8080/singer -H 'Content-Type: application/json' -d '{"first_name":"Cat","last_name":"Meow", "singer_id": 6}'
  1. ターミナルで以下のコマンドを実行して GET をテストします。
curl -X GET http://localhost:8080/singer?singer_id=6
  1. 削除をテストする: 次のコマンドを実行して、エントリを削除してみます。必要に応じて、item-id の値を変更します。
curl -X DELETE http://localhost:8080/singer?singer_id=6
    This throws an error message
500 Internal Server Error

問題を特定して修正する

  1. デバッグモードで問題を見つけます。次のヒントを参考にしてください。
  • DELETE が意図した結果を返さないため、何か問題があることがわかります。そのため、ブレークポイントは delete_singer メソッドの app.py に設定します。
  • ステップごとに実行し、各ステップの変数を監視して、左側のウィンドウでローカル変数の値を確認します。
  • singer_idrequest.args などの特定の値をモニタリングするには、これらの変数を [Watch] ウィンドウに追加します。
  1. singer_id に割り当てられた値が None であることに注意してください。コードを変更して問題を解決してください。

固定コード スニペットは次のようになります。

@app.route('/delete-singer', methods=['DELETE', 'GET'])
def delete_singer():
    try:
        singer_id = request.args.get('singer_id')
  1. アプリケーションが再起動したら、削除してもう一度テストします。
  2. デバッグ ツールバーの赤い正方形 647213126d7a4c7b.png をクリックして、デバッグ セッションを停止します

6. クリーンアップ

これで、このラボでは、新しい Python アプリケーションをゼロから作成し、コンテナと効果的に連携するように構成しました。その後、従来のアプリケーション スタックで見られるのと同じデベロッパー フローに従って、アプリケーションをリモート GKE クラスタにデプロイし、デバッグしました。

ラボの完了後にクリーンアップするには:

  1. ラボで使用したファイルを削除する
cd ~ && rm -rf ~/music-service
  1. プロジェクトを削除して、関連するすべてのインフラストラクチャとリソースを削除する