GoogleCloudでWebアプリ環境を構築
目次
- 概要
- 全体構成
- 構築手順
- 権限
- ネットワーク
- データベース
- ストレージ
- ビルド&デプロイ
- ロードバランシング
- アプリケーション内容
- ログ&分析
- 今後の展望
概要
Google Cloudに習熟するためにwebアプリ環境を構築してみました
プロジェクト内で必要なインフラリソースを立ち上げ、(中身はほぼない)pythonアプリを動かしてみます
全体構成
今回はこちらのアーキテクチャで構成します
ポイントは以下
- Cloud Load Balancingでインターネットからのアクセスを受け取り、動的コンテンツ(Cloud Run)と静的コンテンツ(Cloud Storage)に割り振る
- アプリケーションは、運用コストが低くスケールも簡単なCloud Run上で実行
- アプリケーションはモノリシックな単一サービスとし、データベース接続(Cloud SQL)、画像等のリソース獲得(Cloud Storage)、ログ出力(Cloud Logging)を行う
- ビルドとデプロイはCloud Buildで実行。Githubリポジトリからクローンし、ビルドしたコンテナイメージをCloud Runに、静的コンテンツをCloud Storageにデプロイする
構築手順
以下の手順で構築します
なお、リージョンはCloud Buildを除いてすべてasia-northeast1(東京)にします
基本的にはGoogleの推奨構成に従って構築しています
権限
あらかじめ以下のサービスアカウントを作成し、プロジェクトに紐づけてロールを付与します
<アプリケーション用サービスアカウント>
ロール:「Cloud SQL クライアント」「ログ書き込み」
<SQL中継サーバ用サービスアカウント>
ロール:「Cloud SQL クライアント」
ネットワーク
カスタムVPCネットワークを作成しておきます
サブネットは2種類
- Cloud Run接続用 →IP範囲は「/28」、限定公開のGoogleアクセスはOFF
- SQL中継サーバ設置用 →限定公開のGoogleアクセスはON
ファイアウォールはデフォルトで表示されている以下3つを有効にしておきます
- ~-allow-custom (VPC内のリソース同士の接続許可)
- ~-allow-icmp (ping等の許可)
- ~-allow-ssh (SSH接続の許可)
Cloud SQLと接続するために「プライベートサービスアクセス」を設定し、Service producer VPC networkとピアリングで接続します
割当IPはサブネットのIPと重複しないようにします
Cloud RunをVPCに接続するためにサーバーレスVPCアクセスを作成しておきます
defaultのVPCは削除しておきます
データベース
データベースはCloud SQL(MySQL)を使用します
インスタンス作成時に「接続」の項目でプライベートIPをONにし、パブリックIPをOFFにします
接続に必要なMySQLのユーザー名とパスワードはSecret Managerにそれぞれ保存します
それぞれのシークレットに紐づけて、作成した2つのサービスアカウント「アプリケーション用サービスアカウント」と「SQL中継サーバ用サービスアカウント」に、「Secret Manager のシークレットアクセサー」のロールを付与します
読み取り負荷の分散を想定してレプリケーションを設定します
コンソールから同じインスタンス設定で「リードレプリカを作成」すれば簡単に作れます
Cloud Runから接続するために、MarketplaceでCloud SQL Admin APIを有効化しておきます
データベースへの直接続
Cloud SQLに直接CLIで接続するために、VPC内に中継用サーバーをCompute Engineでたてます
Marketplaceで「MySQL 8」を選択してデプロイすると、MySQL client等がインストールされた状態でインスタンスを作成できます
デプロイ成功後にサービスアカウントに「SQL中継サーバ用サービスアカウント」を設定し、外部IPをOFFにして外部からのアクセスを防ぎます
DBを直接更新/参照するときはこのCompute EngineにSSH接続しそこからMySQLに接続します
gcloud compute ssh <Compute Engineインスタンス名> --zone=<ゾーン名>
↓ SSH接続後
gcloud secrets versions access <version番号> --secret="<DBユーザー名のシークレットキー>" →DBユーザー名が表示
gcloud secrets versions access <version番号> --secret="<DBパスワードのシークレットキー>" →DBパスワードが表示
↓上記を入力
mysql -u <DBユーザー名> -h <割り振られた内部IP> -p
CLI or コンソールからデータベースを作成し、必要なテーブルはCLIから作成します
ストレージ
以下2つのバケットを作成します
-
静的コンテンツ用の公開バケット
バケット作成後に、バケットに紐づけてallUsersに「Storageオブジェクト閲覧者」のアクセス権を付与することで、インターネットに公開されます -
アプリ上で動的に画像等を扱うための非公開バケット
バケット作成後に、バケットに紐づけてアプリケーション用サービスアカウントに「Storageオブジェクト閲覧者」を付与します
ビルド&デプロイ
Cloud Buildで1タッチでビルド&デプロイを行います
githubへのコミットをトリガーにして実行することも可能ですが、今回は手動実行のみとします
リージョンによって制限がかかることもあるらしく、asia-northeast1ではなくus-central1を選択します
Cloud Buildではサービスアカウントを自動作成してくれますが、以下のロールを付与する必要があります
- Cloud Run管理者
- サービスアカウントユーザー
Artifact Registryにトリガーと同名のリポジトリを作成しておきます
アプリコードをpushしたgithubレポジトリと本番環境用の代入変数「_ENV:prd」を設定して実行するとクローンが始まります
なお、githubに接続するための認証情報は自動でSecret Managerに保存されます
プロジェクトのフォルダ構成は主に以下の通り
root/
├─app/
│ ├─main.py
│ └─他
├─prd/
│ └─config.py
├─storage/
│ ├─resource/
│ │ └─img/
│ │ └─アプリから使用する画像
│ └─web/
│ └─img/
│ └─静的コンテンツ
├─cloudbuild.yaml
├─Dockerfile
├─requirement.txt
└─他
クローンしてくると以下のcloudbuild.yamlが読み込まれ順に実行されます
steps:
# STEP0 ビルド
- name: "gcr.io/cloud-builders/docker"
args: [
"build", "-t",
"${_IMAGE_NAME}",
"--build-arg", "_ENV=${_ENV}",
"."
]
# STEP1 Artifact Registryにプッシュ
- name: "gcr.io/cloud-builders/docker"
args: ["push", "${_IMAGE_NAME}"]
# STEP2 Cloud Runにデプロイ
- name: "gcr.io/google.com/cloudsdktool/cloud-sdk:slim"
entrypoint: gcloud
args:
[
"run",
"deploy", "${_SERVICE}",
"--image", "${_IMAGE_NAME}",
"--region", "${_REGION}",
"--platform", "managed",
"--service-account", "${_SERVICE_ACCOUNT}",
"--ingress", "internal-and-cloud-load-balancing",
"--vpc-connector", "${_VPC_CONNECTOR}",
"--allow-unauthenticated"
]
# STEP3 Cloud Storageに公開コンテンツをアップロード
- name: "gcr.io/cloud-builders/gsutil"
args:
[
"rsync", "-d", "-r",
"storage/web",
"gs://${_OPEN_BUCKET}/"
]
# STEP4 Cloud Storageに非公開コンテンツをアップロード
- name: "gcr.io/cloud-builders/gsutil"
args:
[
"rsync", "-d", "-r",
"storage/resource",
"gs://${_CLOSE_BUCKET}/"
]
# ユーザー定義変数
substitutions:
_SERVICE: "<任意のCloud Runアプリケーション名>"
_REGION: "asia-northeast1"
_SERVICE_ACCOUNT: "<アプリケーション用サービスアカウント(メアド形式)>"
_VPC_CONNECTOR: "projects/${$PROJECT_NUMBER}/locations/${_REGION}/connectors/<サーバレスVPCアクセス名>"
_IMAGE_NAME: "${_REGION}-docker.pkg.dev/${PROJECT_ID}/${TRIGGER_NAME}/${_SERVICE}:${SHORT_SHA}"
_OPEN_BUCKET: "<静的コンテンツ用の公開バケット名>"
_CLOSE_BUCKET: "<動的コンテンツ用の非公開バケット名>"
STEP0
Dockerイメージを作成
実行前に指定した本番環境を示す_ENVはそのまま引数で渡します
ビルドは以下のDockerfileを参照して行われます
FROM python:3.11
ARG _ENV
WORKDIR /project
COPY app app
COPY ${_ENV} app
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r ./requirements.txt
CMD ["uvicorn", "app.main:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]
python環境をベースとしたコンテナで、appフォルダ、prdフォルダ内の本番環境用の設定ファイル、必要なライブラリが記載されたrequirement.txtをコピーし、必要ライブラリをインストール
コンテナ起動時にアプリケーションサーバuvicornを起動するようCMDで指示しておきます
STEP1
DockerイメージをArtifact Registryにコミット
STEP2
Cloud Runにデプロイして起動
以下の引数を指定
"--service-account" →アプリケーション用サービスアカウントを指定
"--ingress" →ロードバランサからアクセスさせるために"internal-and-cloud-load-balancing"と設定
"--vpc-connector" →Cloud SQLに接続するためにVPCコネクタを指定
STEP3
静的な画像等のコンテンツをCloud Storageにデプロイ
STEP4
動的に扱う画像等のコンテンツをCloud Storageにデプロイ
ロードバランシング
グローバル外部アプリケーションロードバランサを選択します
(リージョン外部アプリケーションロードバランサだとCloud Storageが使えないみたい)
バックエンドサービスには、アプリを乗せるCloud Runを設定したネットワークエンドポイントグループを指定
バックエンドバケットには、静的コンテンツを乗せるCloud Storageの公開バケットを指定
ルーティングルールには以下を指定
- ホスト:「*」、パス:「/*」、バックエンド:アプリを乗せたCloud Run
- ホスト:「*」、パス:「/img/*」、バックエンド:作成した公開バケット
ここで割り振られた外部IPアドレスでアプリケーションにアクセスすることができます
アプリケーション内容
アプリケーションから各Google Cloudサービスに接続する方法を簡単に記載します
いずれもCloudクライアントのライブラリを使用して接続します
データベース接続
pythonのSQLAlchemyからCloudクライアントを使用します
from google.cloud import secretmanager
from google.cloud.sql.connector import Connector, IPTypes
import pymysql
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
def get_secret(secret_id:str, version_id:int) -> str:
client = secretmanager.SecretManagerServiceClient()
name = f"projects/<プロジェクト番号>/secrets/{secret_id}/versions/{version_id}"
response = client.access_secret_version(request={"name": name})
return response.payload.data.decode("UTF-8")
connector = Connector(IPTypes.PRIVATE)
def getconn() -> pymysql.connections.Connection:
return connector.connect(
"<プロジェクトID:リージョン名:DBインスタンス名>",
"pymysql",
user=get_secret("<DBユーザーシークレット名>", <シークレットバージョン番号>),
password=get_secret("<DBパスシークレット名>", <シークレットバージョン番号>),
db="<データベース名>"
)
db_engine = create_engine("mysql+pymysql://", creator=getconn)
Session = sessionmaker(autocommit=False, autoflush=False, bind=db_engine)
db_session = Session()
データの読み取り時は接続先のDBインスタンスの名前をリードレプリカのものに変更して、db_sessionを取得します
ストレージ接続
以下の使い方で画像のバイナリが取得できます
from google.cloud import storage
bucket = storage.Client().bucket(<バケット名>)
img_bytes = bucket.blob(<画像パス名>).download_as_bytes()
ログ出力
アプリ内で何らかのアクションが起きる時にログ出力をして、Cloud Loggingに送ります
from google.cloud import logging
logger = logging.Client().logger(<ログ名>)
logger.log_struct(<ログに記録したい内容のdictionary>, severity="INFO")
<ログ名>は「applicationlog_create-user-data」のように
- アプリケーションから出力されたログであること
- 具体的なアクション内容(上記の場合はユーザー作成)
が分かる文字列になっていると良いと思います
<ログに記録したい内容のdictionary>は
{"user_id":1, "user_name":"名前"}
のように必要な情報を入れ込むイメージです
環境について
ビルド&デプロイの項目に記載したように、今回のGoogle Cloud環境では本番を想定しており「prd」フォルダ内の設定ファイルをコピーしてデプロイしています
上記の環境に依存する部分のソースコードはprdフォルダ内に記載し、デフォルトフォルダ内の設定ファイルにはローカル開発用の設定(例えばsqliteやローカルファイルを取得するような内容)にして、環境に応じて切り替わるようにしておくと良いでしょう
ログ&分析
出力されたログはログエクスプローラーで簡単に見ることができますが
sinkでBigQueryに送ることでクエリーによる分析も可能となります
コンソールの「ロギング」内の「ログルーター」から以下の内容のsinkを設定します
resource.type = "cloud_run_revision" AND
logName =~ "^projects/<プロジェクトID>/logs/applicationlog_" AND
severity = "INFO"
これによりログで記録した内容に応じたテーブルが自動で作成されクエリによる分析が可能となります
今後の展望
今回はベーシックなwebアプリケーションの環境構築を行ったので、今後はより踏み込んで以下を試してみたいな~と思います
- Vertex AIを使った機械学習パイプライン構築
- GKEでマイクロサービス環境構築
- ビルド時の自動テスト、ブルーグリーンデプロイ
- Cloud Monitorで監視
- Memorystoreでキャッシュ
- TerraFormを使った管理
Discussion