👏

GoogleCloudでWebアプリ環境を構築

2024/01/10に公開

目次

  1. 概要
  2. 全体構成
  3. 構築手順
    1. 権限
    2. ネットワーク
    3. データベース
    4. ストレージ
    5. ビルド&デプロイ
    6. ロードバランシング
    7. アプリケーション内容
    8. ログ&分析
  4. 今後の展望

概要

Google Cloudに習熟するためにwebアプリ環境を構築してみました
プロジェクト内で必要なインフラリソースを立ち上げ、(中身はほぼない)pythonアプリを動かしてみます

全体構成

今回はこちらのアーキテクチャで構成します

ポイントは以下

  1. Cloud Load Balancingでインターネットからのアクセスを受け取り、動的コンテンツ(Cloud Run)と静的コンテンツ(Cloud Storage)に割り振る
  2. アプリケーションは、運用コストが低くスケールも簡単なCloud Run上で実行
  3. アプリケーションはモノリシックな単一サービスとし、データベース接続(Cloud SQL)、画像等のリソース獲得(Cloud Storage)、ログ出力(Cloud Logging)を行う
  4. ビルドとデプロイは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が読み込まれ順に実行されます

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を参照して行われます

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