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 を生成できます。または、ご自身で試して、利用可能かどうかを確認することもできます。このステップ以降は変更できず、プロジェクトを通して同じ 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 として Cloud Workstations も使用します。次の設定スクリプトは、このインフラストラクチャを準備します。

  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 Console で 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"

このメッセージが表示されたら、[許可] ボタンをクリックして、ワークステーションにコピー&ペーストできるようにします。

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

プロンプトが表示されたら、矢印キーでカーソルを移動し、スペースバーでオプションを選択します。

次のいずれかを選択:

  • ポートの 8080
  • y をクリックして構成を保存します。

Skaffold 構成を更新する

  • デフォルトのアプリケーション名を変更する
  • skaffold.yaml を開く
  • 現在 dockerfile-image として設定されているイメージ名を選択します。
  • 右クリックして [すべての出現箇所を変更] を選択します。
  • 新しい名前を python-app として入力します。
  • ビルド セクションをさらに編集して、
  • 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. このラボのプロジェクト ID を Qwiklabs ページからコピーします。

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. 上部に表示されるパネルで、[Kubernetes で実行] を選択します。プロンプトが表示されたら、[Yes] を選択して現在の Kubernetes コンテキストを使用します。

bfd65e9df6d4a6cb.png

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

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

9c87ccbf5d06f50a.png

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

804abc8833ffd571.png

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

  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 ビューで、デバッグ可能なコンテナが起動していることを確認します。
**************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. 停止ボタン 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 に安全に接続するには、Workload Identity を使用するようにアプリケーションを設定します。これにより、アプリケーションは独自のサービス アカウントとして機能し、データベースにアクセスする際に個別の権限を持つことができます。

  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. ビルドとテストが完了すると、[出力] タブに 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 などの特定の値を監視するには、これらの変数を [ウォッチ] ウィンドウに追加します。
  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. プロジェクトを削除して、関連するインフラストラクチャとリソースをすべて削除する