株式会社G-genのブログ - TECH PLAY

TECH PLAY

株式会社G-gen

株式会社G-gen の技術ブログ

全888件

当記事では、Cloud Vision API を用いて PDF ファイルからテキストを抽出し、Google Cloud の Generative AI モデルが利用できる Vertex AI PaLM API を呼び出して抽出したテキストの要約をやってみたので解説します。 前提知識 Generative AI Support on Vertex AI Cloud Vision API 今回扱うデータ 構成図 プロンプト設計 概要 コンテキスト 入出力例の追加 準備 ディレクトリ構成 main.tf gcf_source_code/pdf_to_text main.py requirements.txt gcf_source_code/sammarize main.py requirements.txt 動作検証 検証データ 実行 Cloud Vision API × Vertex AI PaLM API 前提知識 Generative AI Support on Vertex AI 先日 Vertex AI でも Generative AI がサポートされました。Generative AI モデル (基盤モデル) の裏側は PaLM 2 が利用されており、多言語、推論、コーディング機能が強化された最先端の大規模言語モデル (LLM) です。 Vertex AI で Generative AI がサポートされたことで、Vertex AI のエンドポイントから基盤モデルを呼び出せるようになりました。 Vertex AI の Generative AI サポートについての詳細は以下の記事をご参照下さい。 blog.g-gen.co.jp 2023 年 7 月現在、Vertex AI PaLM API は日本語未対応ですが、筆者の環境は Trusted Testers プログラムに参加中のため日本語にも対応しております。 Cloud Vision API Cloud Vision API とは、事前トレーニング済み Vision API モデル使用して、画像内のオブジェクトの検知や OCR だけでなく、PDF / TIFF ファイル中のテキスト抽出等も行なえます。 その他の機能、また詳細については以下のドキュメントをご参照下さい。 参考: Features list また、Cloud Vision API を用いたやってみた記事として、以下のようなものもあるので興味があれば御覧ください。 blog.g-gen.co.jp 今回扱うデータ 今回扱う PDF ファイルは、ダミーで作成した日報データです。 daily_report.pdf 構成図 今回の構成は以下のとおりです。 構成図 PDF 用バケットに PDF ファイル (日報) がアップロードされたことをトリガーに、 PDF からテキストを抽出する Cloud Functions 関数が起動し、抽出したテキストデータをテキスト用バケットに格納します。 次に、テキスト格納用バケットにデータがアップロードされたことをトリガーに、文章を要約する Cloud Functions 関数が起動し、要約したデータが要約された文章用バケットに格納します。 プロンプト設計 概要 プロンプトとは、簡単に言うと大規模言語モデル (LLM) に送信するリクエストのことです。 Vertex AI PaLM API からより良い回答を生成してもらうためには、プロンプトの設計が非常に重要です。 参考: プロンプト設計 今回は、自然な会話やチャットボット等のユースケースに特化した chat-bison@001 (チャット用言語モデル) を使用します。 チャット用言語モデルのプロンプトには、次の 3 つのコンポーネントで構成されます。 メッセージ [必須] コンテキスト [オプション] 入出力例の追加 [オプション] コンテキスト コンテキスト とは、モデルの応答方法を指示したり、モデルのペルソナを指定したりできます。 よって今回は、以下のようなコンテキストを設定します。 あなたはプロの編集者です。以下の制約条件に従って、入力する文章を要約してください。 制約条件 ・300文字以内にまとめて要約した文章として出力。 ・文章の意味を変更しない。 ・架空の表現や言葉を使用しない。 入出力例の追加 入出力例の追加 とは、特定の入力例と、その入力に対するモデルの出力例、つまり入出力のペアリストのことです。 PDF から抽出されるテキストデータのサンプルとして、以下を入力例とします。 日付名前2023年7月24日営業 太郎日報業務報告お疲れ様です。 本日の活動について報告いたします。本日は、 当社の製品に関心を示していただいた新規のお客様への訪問を中心に行いました。 まず、 午前中にはA社を訪問し、当社製品の特徴と価格競争力についてプレゼンテーションを行いました。 彼らは特に当社の製品のコストパフォーマンスに感心していましたが、 具体的な購入の意志は明らかになりませんでした。 引き続き 情報提供を行い、 ビジネスを進展させるべく、 交渉を続けます。午後は、新たに興味を示してくださったB社とのミーティングを行いました。 当社の製品についての詳細な質問や、予算に関する議論が交わされました。B社は即決ではありませんでしたが、 当社の製品に強い興味を持っていることが伺え、有望な見込み客と判断しています。その他、進行中のC社との契約交渉については、一部微調整が必要な部分が見つかりました。 来週中には改めて会議を設定し、これをクリアにする予定です。全体として、本日は新規顧客開拓と既存顧客との継続的な関係強化に重点を置いた一日となりました。 明日はさらなる顧客訪問と情報収集、ならびに製品のプレゼンテーションを行う予定です。 ご支援のほど、よろしくお願い申し上げます。以上、本日の報告となります。 モデルの出力例としては、「日付 : 」「名前 : 」「要約 : 」を分けて出力してもらえるよう、以下のように設定します。 日付:2023年7月24日 名前:営業 太郎 要約:本日は、当社の製品に関心を示していただいた新規のお客様への訪問を中心に行いました。午前中にはA社を訪問し、当社製品の特徴と価格競争力についてプレゼンテーションを行いました。午後は、新たに興味を示してくださったB社とのミーティングを行いました。その他、進行中のC社との契約交渉については、一部微調整が必要な部分が見つかりました。明日はさらなる顧客訪問と情報収集、ならびに製品のプレゼンテーションを行う予定です。 準備 ディレクトリ構成 開発環境は Cloud Shell を用いて行います。ディレクトリ構造は以下のとおりです。 terraform ディレクトリ配下は、以下のとおりです。 terraform |-- gcf_source_code | |-- pdf_to_text | | |-- main.py | | `-- requirements.txt | ` -- sammarize | |-- main.py | `-- requirements.txt ` -- main.tf main.tf main.tf には Terraform のコードを記述しています。 locals { terraform_service_account = $ { Terraform 実行に使われるサービスアカウントのメールアドレス } project_name = $ { プロジェクト名 } project_id = $ { プロジェクト ID } folder_id = $ { フォルダ ID } billing_account_id = $ { 請求先アカウント ID } } # terraform & provider の設定 terraform { required_providers { google = { source = "hashicorp/google" version = ">= 4.0.0" } } required_version = ">= 1.3.0" backend "gcs" { bucket = $ { tfstate ファイルを格納する Cloud Storage バケット名 } impersonate_service_account = $ { Terraform 実行に使われるサービスアカウントのメールアドレス } } } # サービスアカウント権限借用の設定 provider "google" { alias = "impersonation" scopes = [ "https://www.googleapis.com/auth/cloud-platform" , "https://www.googleapis.com/auth/userinfo.email" , ] } data "google_service_account_access_token" "default" { provider = google.impersonation target_service_account = local.terraform_service_account scopes = [ "userinfo-email" , "cloud-platform" ] lifetime = "1200s" } # Google プロバイダの設定 provider "google" { project = local.project_id region = "asia-northeast1" access_token = data.google_service_account_access_token.default.access_token request_timeout = "60s" } ###################################### ### プロジェクトの作成と API の有効化 ### ###################################### # プロジェクトの作成 resource "google_project" "poc" { name = local.project_name project_id = local.project_id folder_id = local.folder_id billing_account = local.billing_account_id } # API の有効化 module "tenant_a_project_services" { source = "terraform-google-modules/project-factory/google//modules/project_services" version = "14.2.1" project_id = google_project.poc.project_id enable_apis = true activate_apis = [ "iam.googleapis.com" , "cloudbuild.googleapis.com" , "run.googleapis.com" , "cloudfunctions.googleapis.com" , "pubsub.googleapis.com" , "eventarc.googleapis.com" , "artifactregistry.googleapis.com" , "storage.googleapis.com" , "vision.googleapis.com" , "aiplatform.googleapis.com" ] disable_services_on_destroy = false } ####################################### ### サービスアカウントの作成と権限の付与 ## ####################################### # Cloud Functions 用サービスアカウントの作成と権限付与 resource "google_service_account" "sa_gcf" { project = google_project.poc.project_id account_id = "sa-gcf" display_name = "Cloud Functions 用サービスアカウント" } resource "google_project_iam_member" "invoke_gcf" { project = google_project.poc.project_id role = "roles/run.invoker" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_project_iam_member" "storage_admin" { project = google_project.poc.project_id role = "roles/storage.admin" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_project_iam_member" "event_receiving" { project = google_project.poc.project_id role = "roles/eventarc.eventReceiver" member = "serviceAccount:${google_service_account.sa_gcf.email}" depends_on = [ google_project_iam_member.invoke_gcf ] } resource "google_project_iam_member" "artifactregistry_reader" { project = google_project.poc.project_id role = "roles/artifactregistry.reader" member = "serviceAccount:${google_service_account.sa_gcf.email}" depends_on = [ google_project_iam_member.event_receiving ] } resource "google_project_iam_member" "vertex_ai_user" { project = google_project.poc.project_id role = "roles/aiplatform.user" member = "serviceAccount:${google_service_account.sa_gcf.email}" } # Eventarc のサービスアカウントに権限付与 resource "google_project_iam_member" "serviceAccount_token_creator" { project = google_project.poc.project_id role = "roles/iam.serviceAccountTokenCreator" member = "serviceAccount:service-${google_project.poc.number}@gcp-sa-pubsub.iam.gserviceaccount.com" depends_on = [ module.tenant_a_project_services ] } # Cloud Storage のサービスアカウントに権限付与 data "google_storage_project_service_account" "gcs_account" { project = google_project.poc.project_id depends_on = [ module.tenant_a_project_services ] } resource "google_project_iam_member" "gcs_pubsub_publishing" { project = google_project.poc.project_id role = "roles/pubsub.publisher" member = "serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}" } ################################ ### バケットとオブジェクトの作成 ### ################################ # Cloud Functions のソースコード格納用バケットの作成 resource "google_storage_bucket" "source_gcf" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-source-gcf" force_destroy = true } # Cloud Functions で使うソースコードを ZIP 化 data "archive_file" "pdf_to_text" { type = "zip" source_dir = "./gcf_source_code/pdf_to_text" output_path = "./zip_source_code/pdf_to_text.zip" } data "archive_file" "sammarize" { type = "zip" source_dir = "./gcf_source_code/sammarize" output_path = "./zip_source_code/sammarize.zip" } # ZIP 化したソースコードをバケットに追加 resource "google_storage_bucket_object" "pdf_to_text" { name = "pdf-to-text.${data.archive_file.pdf_to_text.output_md5}.zip" bucket = google_storage_bucket.source_gcf.name source = data.archive_file.pdf_to_text.output_path } resource "google_storage_bucket_object" "sammarize" { name = "sammarize.${data.archive_file.sammarize.output_md5}.zip" bucket = google_storage_bucket.source_gcf.name source = data.archive_file.sammarize.output_path } # pdf_data バケットの作成 resource "google_storage_bucket" "pdf_data" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-pdf-data" force_destroy = true } # text_data バケットの作成 resource "google_storage_bucket" "text_data" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-text-data" force_destroy = true } # summarized_text_data バケットの作成 resource "google_storage_bucket" "summarized_text_data" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-summarized-text-data" force_destroy = true } ############################ ### Cloud Functions 作成 ### ############################ # pdf_to_text 関数 resource "google_cloudfunctions2_function" "pdf_to_text" { depends_on = [ google_project_iam_member.event_receiving, google_project_iam_member.artifactregistry_reader, google_project_iam_member.serviceAccount_token_creator ] name = "pdf-to-text" location = "asia-northeast1" description = "PDF からテキストを取得し text_data バケットに格納する関数" build_config { runtime = "python310" entry_point = "main" # Set the entry point in the code source { storage_source { bucket = google_storage_bucket.source_gcf.name object = google_storage_bucket_object.pdf_to_text.name } } } service_config { max_instance_count = 3 min_instance_count = 1 available_memory = "256M" timeout_seconds = 60 environment_variables = { DESTINATION_BUCKET_NAME = google_storage_bucket.text_data.name } service_account_email = google_service_account.sa_gcf.email } event_trigger { trigger_region = "asia-northeast1" event_type = "google.cloud.storage.object.v1.finalized" retry_policy = "RETRY_POLICY_DO_NOT_RETRY" service_account_email = google_service_account.sa_gcf.email event_filters { attribute = "bucket" value = google_storage_bucket.pdf_data.name } } } # sammarize 関数 resource "google_cloudfunctions2_function" "sammarize" { depends_on = [ google_project_iam_member.event_receiving, google_project_iam_member.artifactregistry_reader, google_project_iam_member.serviceAccount_token_creator ] name = "sammarize" location = "asia-northeast1" description = "Vertex AI PaLM API でテキストを要約し summarized_text_data バケットに格納する関数" build_config { runtime = "python310" entry_point = "main" # Set the entry point in the code source { storage_source { bucket = google_storage_bucket.source_gcf.name object = google_storage_bucket_object.sammarize.name } } } service_config { max_instance_count = 3 min_instance_count = 1 available_memory = "256M" timeout_seconds = 60 environment_variables = { PROJECT_ID = google_project.poc.project_id DESTINATION_BUCKET_NAME = google_storage_bucket.summarized_text_data.name } service_account_email = google_service_account.sa_gcf.email } event_trigger { trigger_region = "asia-northeast1" event_type = "google.cloud.storage.object.v1.finalized" retry_policy = "RETRY_POLICY_DO_NOT_RETRY" service_account_email = google_service_account.sa_gcf.email event_filters { attribute = "bucket" value = google_storage_bucket.text_data.name } } } gcf_source_code/pdf_to_text main.py gcf_source_code/pdf_to_text には、PDF ファイルからテキストを抽出する Cloud Functions 関数のソースコードを格納しています。 import json import re import os from cloudevents.http import CloudEvent import functions_framework from google.cloud import vision DESTINATION_BUCKET_NAME = os.environ.get( "DESTINATION_BUCKET_NAME" ) # クライアントの初期化 vision_client = vision.ImageAnnotatorClient() def async_detect_document ( gcs_source_uri, gcs_destination_uri, ): # 入力設定を構成 mime_type = "application/pdf" gcs_source = vision.GcsSource(uri=gcs_source_uri) input_config = vision.InputConfig(gcs_source=gcs_source, mime_type=mime_type) # 出力設定を構成 feature = vision.Feature( type_=vision.Feature.Type.DOCUMENT_TEXT_DETECTION ) gcs_destination = vision.GcsDestination(uri=gcs_destination_uri) output_config = vision.OutputConfig( gcs_destination=gcs_destination ) # 非同期リクエストを実行 async_request = vision.AsyncAnnotateFileRequest( features=[feature], input_config=input_config, output_config=output_config ) # operations リソースでステータスを確認 operation = vision_client.async_batch_annotate_files(requests=[async_request]) print ( "Waiting for the operation to finish." ) # 非同期処理が完了していない場合、最大 timeout 秒まで待機 operation.result(timeout= 420 ) return "ok" @ functions_framework.cloud_event def main (cloud_event: CloudEvent): # CloudEvent から渡されたデータを取得 data = cloud_event.data bucket_name = data[ "bucket" ] file_name = data[ "name" ] no_extension_file_name = file_name.split( '.' )[ 0 ] gcs_source_uri = f "gs://{bucket_name}/{file_name}" gcs_destination_uri = f "gs://{DESTINATION_BUCKET_NAME}/{no_extension_file_name}/" async_detect_document( gcs_source_uri = gcs_source_uri, gcs_destination_uri = gcs_destination_uri, ) return "ok" PDF からテキストを取得する際、 AsyncBatchAnnotateFilesRequest メソッドを利用していますが、こちらは非同期でリクエストが行われます。 operations リソースを用いることで、そのステータスが確認でき、尚タイムアウトの指定も可能となります。 また、今回は検証のため対応しておりませんが、出力されるテキストが大きくなると複数ファイルに分かれて Cloud Storage に書き込まれる可能性がある為、本番運用時はこのあたりの考慮も必要となります。本検証は、1 ファイルのインプットにつき 1 ファイルのアウトプットのみに対応した構成となっています。 参考: GcsDestination requirements.txt functions-framework==3.* cloudevents==1.9.0 google-cloud-vision==3.4.4 gcf_source_code/sammarize main.py gcf_source_code/sammarize には、テキストを要約する Cloud Functions 関数のソースコードを格納しています。 import json import os from cloudevents.http import CloudEvent import functions_framework import vertexai from vertexai.preview.language_models import ChatModel, InputOutputTextPair from google.cloud import storage PROJECT_ID = os.environ.get( "PROJECT_ID" ) DESTINATION_BUCKET_NAME = os.environ.get( "DESTINATION_BUCKET_NAME" ) # 基盤モデルとストレージクライアントの初期化 vertexai.init(project=PROJECT_ID, location= "us-central1" ) chat_model = ChatModel.from_pretrained( "chat-bison@001" ) storage_client = storage.Client() def download_json_from_bucket (bucket_name, blob_name): # バケットを取得 bucket = storage_client.get_bucket(bucket_name) # オブジェクトを取得 blob = bucket.blob(blob_name) json_string = blob.download_as_bytes().decode( "utf-8" ) response = json.loads(json_string) return response def summarize_text (text): # 言語モデルのリクエストパラメラータを指定 parameters = { "temperature" : 0.2 , "max_output_tokens" : 1024 , "top_p" : 0.8 , "top_k" : 40 } # コンテキストと INPUT / OUTPUT の例を追加 chat = chat_model.start_chat( context= """あなたはプロの編集者です。以下の制約条件に従って、入力する文章を要約してください。 制約条件 ・300文字以内にまとめて要約した文章として出力。 ・文章の意味を変更しない。 ・架空の表現や言葉を使用しない。 #出力形式 日付: 名前: 要約した文章:""" , examples=[ InputOutputTextPair( input_text= """日付名前2023年7月24日営業 太郎日報業務報告お疲れ様です。 本日の活動について報告いたします。本日は、 当社の製品に関心を示していただいた新規のお客様への訪問を中心に行いました。 まず、 午前中にはA社を訪問し、当社製品の特徴と価格競争力についてプレゼンテーションを行いました。 彼らは特に当社の製品のコストパフォーマンスに感心していましたが、 具体的な購入の意志は明らかになりませんでした。 引き続き 情報提供を行い、 ビジネスを進展させるべく、 交渉を続けます。午後は、新たに興味を示してくださったB社とのミーティングを行いました。 当社の製品についての詳細な質問や、予算に関する議論が交わされました。B社は即決ではありませんでしたが、 当社の製品に強い興味を持っていることが伺え、有望な見込み客と判断しています。その他、進行中のC社との契約交渉については、一部微調整が必要な部分が見つかりました。 来週中には改めて会議を設定し、これをクリアにする予定です。全体として、本日は新規顧客開拓と既存顧客との継続的な関係強化に重点を置いた一日となりました。 明日はさらなる顧客訪問と情報収集、ならびに製品のプレゼンテーションを行う予定です。 ご支援のほど、よろしくお願い申し上げます。以上、本日の報告となります。""" , output_text= """日付:2023年7月24日 名前:営業 太郎 要約:本日は、当社の製品に関心を示していただいた新規のお客様への訪問を中心に行いました。午前中にはA社を訪問し、当社製品の特徴と価格競争力についてプレゼンテーションを行いました。午後は、新たに興味を示してくださったB社とのミーティングを行いました。その他、進行中のC社との契約交渉については、一部微調整が必要な部分が見つかりました。明日はさらなる顧客訪問と情報収集、ならびに製品のプレゼンテーションを行う予定です。""" ) ] ) # 基盤モデルにリクエストを実行 response = chat.send_message(text, **parameters) return response.text def upload_text_to_bucket (bucket_name, blob_name, text_data): # バケットを取得 bucket = storage_client.get_bucket(bucket_name) # バケットにアップロードするためのblobを作成 blob = bucket.blob(blob_name) # テキストデータをアップロード blob.upload_from_string(text_data) return "OK" @ functions_framework.cloud_event def main (cloud_event: CloudEvent): # CloudEvent から渡されたデータを取得 data = cloud_event.data print (data) bucket_name = data[ "bucket" ] blob_name = data[ "name" ] # "Sheet2/output-1-to-1.json" folder_name = blob_name.split( '/' )[ 0 ] response = download_json_from_bucket( bucket_name = bucket_name, blob_name = blob_name ) # json からテキストデータの取得 first_page_response = response[ "responses" ][ 0 ] response_text = first_page_response[ "fullTextAnnotation" ][ "text" ] # 改行と「|」の削除 corrected_text = response_text.replace( ' \n ' , '' ).replace( '|' , '' ) print (f "=====原文=====" ) print (corrected_text) print (f "=====サマリ=====" ) summarized_text = summarize_text(corrected_text) print (summarized_text) upload_text_to_bucket( bucket_name = DESTINATION_BUCKET_NAME, blob_name = f "{folder_name}.txt" , text_data = summarized_text.encode( "shift_jis" ) ) return "ok" Vertex AI PaLM API エンドポイントへのリクエストには、先程のプロンプト設計で作成したコンテキストと入出力例の追加を行っています。 requirements.txt functions-framework==3.* cloudevents==1.9.0 google-cloud-vision==3.4.4 google-cloud-storage==2.10.0 google-cloud-aiplatform==1.28.1 動作検証 検証データ 以下の PDF ファイルで動作検証を行います。尚、業務報告内の文字数は 736 文字となります。 daily_report.pdf 実行 PDF 格納バケットに daily_report.pdf をアップロードします。 格納バケットコンソール画面 直後に Cloud Storage トリガー経由で Cloud Functions が起動し、最終的に要約されたテキスト格納バケットにオブジェクトが生成されました。 要約されたテキスト格納バケット 中身は以下のようになってました。 daily_report.txt 入力コンテキストの制限事項には 300 文字以内で要約するよう記載していましたが、要約後のテキストでは 411 文字で出力されました 。 圧縮率 (または要約率) 56% 。 何度か実行してみましたが 300 文字以内で要約することはできなかったため、制限事項に正確に従うことは現状難しいのかと思います。 しかし要約内容について、 重要な箇所は漏れなく記載されており、文章全体にも違和感なく要約 されています。 G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
G-gen の武井です。当記事では Cloud Build を使って Terraform 実行を自動化する方法を紹介します。 Cloud Build で Terraform 実行を自動化 はじめに 当記事の概要 前提知識 GitHub Terraform Cloud Build Cloud Build トリガー Terraform / Cloud Build 詳細 アーキテクチャ 設定手順 GitHub リポジトリとの連携 手順 Cloud Build トリガーの作成 コマンド解説 ソースコードの作成 Terraform ソースコード ビルド構成ファイル 動作確認 dev ブランチへのプッシュ main ブランチへマージ 関連記事 はじめに 当記事の概要 GitHub の任意のブランチへのプッシュを契機に Terraform を実行する CI/CD パイプラインを、GitHub と Code Build のみで実装することができます。 当記事ではその手順についてご紹介します。 前提知識 GitHub GitHub とは、開発者がコードを共有、管理、コラボレーションするためのプラットフォームです。 Git ベースのバージョン管理システムに加え、問題追跡、機能リクエスト、タスク管理、コミュニティ構築などのツールも提供しています。 Terraform Terraform とは、HashiCorp 社が開発したオープンソースの Infrastructure as Code (IaC) ツールです。 Terraform では、独自フォーマットの設定ファイルに宣言型のコードを記述することで、仮想マシン、ストレージ、ネットワーク等の各種リソースを管理できます。また、作成した各種リソースの状態 (state) についてもファイルとして保存されるため、実環境と設定ファイルの差分が把握できます。 ファイルでクラウドリソースを管理できるため、 バージョン管理や CI/CD (継続的インテグレーション / 継続的デリバリ) を可能にします。また、Google Cloud (旧称 GCP) だけでなく Amazon Web Services (AWS) や Microsoft Azure といった主要なパブリッククラウドにも対応しています。 Cloud Build Cloud Build とは、Google Cloud のフルマネージドなサーバレス CI/CD(継続的インテグレーション / 継続的デリバリー)プラットフォームです。 開発者はソースコードからインフラやアプリケーションを高速かつ確実にビルド、テスト、デプロイすることができ、効率的な開発ライフサイクルの実現をサポートします。 Cloud Build トリガー Cloud Build トリガー とはビルドプロセスを自動的に起動させる機能です。 例えば任意のブランチにプッシュやプルリクエストといったイベントが発生した際、それをトリガーに予め定義したビルドプロセスを自動的に実行します。 ビルドプロセスは YAML または JSON 形式のビルド構成ファイルで定義します。 Terraform / Cloud Build 詳細 Terraform や Cloud Build に関する詳細は以下の記事で解説しております。 こちらも合わせて参照ください。 blog.g-gen.co.jp blog.g-gen.co.jp アーキテクチャ 今回の例ではリポジトリに GitHub を使用し、任意のブランチへのプッシュイベントをトリガーに Terraform が動作します。 ローカル側で切り出した dev ブランチをリモート側にプッシュする terraform plan が自動実行 main ブランチにマージする terraform apply が自動実行 詳細は後述しますが、 terraform apply コマンドは main ブランチにマージされた場合にのみ実行されるようにしています。 これは、任意のブランチ (図の例では dev ) にプッシュされたソースコードや terraform plan の実行結果がきちんとレビューされたあとに実行されることを意図しているからです。 任意のブランチへの Push イベントをトリガーとして起動する Cloud Build トリガー 設定手順 GitHub リポジトリとの連携 まず Terraform のソースコードを管理する GitHub リポジトリとの連携を行います。手順は以下の公式ガイドを参考にしています。 参考 : GitHub リポジトリを接続する 手順 Cloud コンソール > Cloud Build トリガー と遷移し、任意のリージョンを選択したら リポジトリを接続 をクリックします。 リポジトリを接続 をクリック GitHub (Cloud Build GitHub アプリ) を選択し、 続行 をクリックします。 GitHub (Cloud Build GitHub アプリ) を選択 Cloud Build GitHub アプリのインストール先となる 組織 または GitHub ユーザー名 を選択します。 アプリのインストール先を選択 All repositories (すべての GitHub リポジトリ) あるいは Only select repositories (特定の GitHub リポジトリ) かを選択したのち、 Install をクリックします。 連携対象のリポジトリを選択 GitHub ユーザーアカウントに MFA 認証が設定されている場合は認証コードを入力します。 認証コードを入力 (MFA 認証が有効な場合) GitHub アカウント と リポジトリ の選択、利用規約への同意 ( チェックボックス ) が完了したら、最後に 接続 をクリックします。 対象のリポジトリを選択したら接続 サンプルトリガーは作成せずに 完了 をクリックします。 サンプルトリガーは作成せずに完了する Cloud Build トリガーの作成 次に Cloud Build トリガーを作成します。今回 Cloud Build トリガーの作成には gcloud コマンドを使用します。 参考 : gcloud beta builds triggers create github 参考 : ビルドトリガーの作成 gcloud beta builds triggers create github \ --name =" push-trigger-terraform " \ --region =" asia-northeast1 " \ --repo-name =" sample-repo-name " \ --repo-owner =" sample-repo-owner-name " \ --branch-pattern =" .* " \ --included-files =" sample/modules/** " \ --build-config =" sample/terraform.yaml " \ --substitutions =" _WORK_DIR "=" sample/ " コマンド解説 上記コマンドを実際の成果物 (Cloud Build トリガー) に照らし合わせて解説します。 Cloud コンソール > Cloud Build > トリガー から確認可能です。 トリガー名とリージョン --name と --region オプションで指定した値で作成されます。 トリガー名とリージョン イベント プッシュイベントをトリガーとする場合、コマンドオプションでの指定は不要です。 イベントのデフォルトはプッシュイベント リポジトリ --repo-name と --repo-owner は GitHub リポジトリの URL に記載されている値を入力します。 repo-name と repo-owner は GitHub リポジトリの URL から確認可能 GitHub リポジトリの URL ブランチ名とビルド構成ファイル --branch-pattern でプッシュ先のブランチを指定しますが、ビルド構成ファイルで制御したいためこちらの設定値は任意 ( .* ) としています。 また、 --included-files で sample/modules ディレクトリ配下のファイル更新に関するプッシュイベントをターゲットにしています。(ファイル構成は後述) プッシュ先ブランチはビルド構成ファイルで制御するため、トリガー設定上は任意とする ビルド構成ファイル --build-config でビルド構成ファイルのパスをルートディレクトリを起点に明示します。(ファイル構成は後述) ビルド構成ファイルのパスを指定 変数 ビルド構成ファイルで使用する変数を --substitutions で指定します。 変数 ( _WORK_DIR ) は Terraform コマンドを実行するディレクトリ ( sample/ ) を定義しています。 ビルド構成ファイルで使用する変数の定義 ソースコードの作成 Terraform のソースコードは GitHub に用意します。コードの編集には コードスペース を使用しており、 /workspaces/sample-repo-name がルートディレクトリに相当します。 @username ➜ /workspaces/sample-repo-name ( dev ) $ pwd /workspaces/sample-repo-name @username ➜ /workspaces/sample-repo-name ( dev ) $ tree . ├── sample │ ├── modules │ │ └── cloud_storage │ │ ├── main.tf │ │ └── variables.tf │ ├── backend.tf │ ├── main.tf │ ├── terraform.tfvars │ ├── terraform.yaml │ ├── variables.tf │ └── versions.tf Terraform ソースコード 以下の Terraform ソースコードを使ってストレージバケットを払い出します。 sample/modules/cloud_storage/main.tf locals { names = [ "cicd-testbucket-01" , ] } resource "google_storage_bucket" "buckets" { for_each = toset (local.names) name = each.value storage_class = "STANDARD" project = var.project_id location = var.region uniform_bucket_level_access = true } sample/modules/cloud_storage/variables.tf ( sample/variables.tf も同じ) variable "project_id" { type = string } variable "region" { type = string } sample/backend.tf terraform { backend "gcs" { bucket = "sample-project-tfstate-bucket" prefix = "terraform/state" } } sample/main.tf module "cicd_test" { source = "./modules/cloud_storage" project_id = var.project_id region = var.region } sample/terraform.tfvars project_id = "sample-project" region = "asia-northeast1" sample/versions.tf provider "google" { project = var.project_id } terraform { required_version = "~> 1.5.0" required_providers { google = { source = "hashicorp/google" version = "~> 4.65.2" } } } ビルド構成ファイル ビルド構成ファイル ( sample/terraform.yaml ) では以下の処理を定義しています。 実行される処理は terraform init / terraform plan / terraform apply の 3つ 各処理は _WORK_DIR 変数で定義した sample/ ディレクトリ上で実行される プッシュ先ブランチが main 以外の場合、 terraform plan までの処理が実行され、最後に echo で定義したメッセージ ( terraform apply がスキップされた旨) を表示して終了する マージ先ブランチが main の場合、 terraform apply までの処理が実行される steps : - id : "tf init" name : "hashicorp/terraform:1.5.0" dir : "$_WORK_DIR" entrypoint : "sh" args : - "-c" - | terraform init -upgrade || exit 1 - id : "tf plan" name : "hashicorp/terraform:1.5.0" dir : "$_WORK_DIR" entrypoint : "sh" args : - "-c" - | terraform plan || exit 1 - id : "tf apply" name : "hashicorp/terraform:1.5.0" dir : "$_WORK_DIR" entrypoint : "sh" args : - "-c" - | if [ "$BRANCH_NAME" = "main" ] ; then terraform apply -auto-approve else echo "***************************************************************************************" echo "terraform apply was skipped because it's not a merge into the main branch." echo "***************************************************************************************" fi 動作確認 dev ブランチへのプッシュ、 main ブランチへのマージでそれぞれの処理が意図通り実行されるかを確認します。 dev ブランチへのプッシュ コードスペース (ローカル) 上に main ブランチをクローンした後に dev ブランチを切り、上記で説明したソースコード一式を作成したらリモートにプッシュします。 以下はその際のコマンド操作履歴です。 @username ➜ /workspaces/sample-repo-name ( dev ) $ history 1 git checkout -b dev 2 git add . 3 git commit -m " add storage bucket " 4 git push --set-upstream origin dev GitHub (リモート側) にソースコードがプッシュされた旨が表示されました。 dev ブランチへのプッシュを検知 次に、 Cloud コンソール > Cloud Build > 履歴 から Cloud Build の実行履歴を確認します。一覧上のステータスが 成功 になっていますので ビルド ID をクリックして詳細を確認します。 ビルド履歴一覧 各処理が成功しており、ログ出力からも意図通り動作していることがわかります。 terraform plan が実行され、ストレージバケットが払い出される旨が表示 main ブランチへのマージではないので、 terraform apply はスキップ 各処理が正常終了 # ログ出力 (重要な部分のみ抜粋) starting build "294c2437-edcb-405c-9b5c-646e23de2fb5" Starting Step #0 - "tf init" Step #0 - "tf init": Step #0 - "tf init": Initializing the backend... Step #0 - "tf init": Step #0 - "tf init": Successfully configured the backend "gcs"! Terraform will automatically Step #0 - "tf init": use this backend unless the backend configuration changes. Step #0 - "tf init": Upgrading modules... Step #0 - "tf init": - cicd_test in modules/cloud_storage Step #0 - "tf init": Step #0 - "tf init": Initializing provider plugins... Step #0 - "tf init": - Finding hashicorp/google versions matching "~> 4.65.2"... Step #0 - "tf init": - Installing hashicorp/google v4.65.2... Step #0 - "tf init": - Installed hashicorp/google v4.65.2 (signed by HashiCorp) Step #0 - "tf init": Step #0 - "tf init": Terraform has been successfully initialized! Step #0 - "tf init": Step #0 - "tf init": You may now begin working with Terraform. Try running "terraform plan" to see Step #0 - "tf init": any changes that are required for your infrastructure. All Terraform commands Step #0 - "tf init": should now work. Step #0 - "tf init": Step #0 - "tf init": If you ever set or change modules or backend configuration for Terraform, Step #0 - "tf init": rerun this command to reinitialize your working directory. If you forget, other Step #0 - "tf init": commands will detect it and remind you to do so if necessary. Finished Step #0 - "tf init" Starting Step #1 - "tf plan" Step #1 - "tf plan": Already have image: hashicorp/terraform:1.5.0 Step #1 - "tf plan": Step #1 - "tf plan": Terraform used the selected providers to generate the following execution Step #1 - "tf plan": plan. Resource actions are indicated with the following symbols: Step #1 - "tf plan": + create Step #1 - "tf plan": Step #1 - "tf plan": Terraform will perform the following actions: Step #1 - "tf plan": Step #1 - "tf plan": # module.cicd_test.google_storage_bucket.buckets["cicd-testbucket-01"] will be created Step #1 - "tf plan": + resource "google_storage_bucket" "buckets" { Step #1 - "tf plan": + force_destroy = false Step #1 - "tf plan": + id = (known after apply) Step #1 - "tf plan": + location = "ASIA-NORTHEAST1" Step #1 - "tf plan": + name = "cicd-testbucket-01" Step #1 - "tf plan": + project = "sample-project" Step #1 - "tf plan": + public_access_prevention = (known after apply) Step #1 - "tf plan": + self_link = (known after apply) Step #1 - "tf plan": + storage_class = "STANDARD" Step #1 - "tf plan": + uniform_bucket_level_access = true Step #1 - "tf plan": + url = (known after apply) Step #1 - "tf plan": } Step #1 - "tf plan": Step #1 - "tf plan": Plan: 1 to add, 0 to change, 0 to destroy. Step #1 - "tf plan": Step #1 - "tf plan": ───────────────────────────────────────────────────────────────────────────── Step #1 - "tf plan": Step #1 - "tf plan": Note: You didn't use the -out option to save this plan, so Terraform can't Step #1 - "tf plan": guarantee to take exactly these actions if you run "terraform apply" now. Finished Step #1 - "tf plan" Starting Step #2 - "tf apply" Step #2 - "tf apply": Already have image: hashicorp/terraform:1.5.0 Step #2 - "tf apply": *************************************************************************************** Step #2 - "tf apply": terraform apply was skipped because it's not a merge into the main branch. Step #2 - "tf apply": *************************************************************************************** Finished Step #2 - "tf apply" PUSH DONE main ブランチへマージ dev ブランチへのプッシュ ( terraform plan の実行結果) は想定通りでしたので、次に main ブランチにマージ (プッシュ) します。 main ブランチにマージ (プッシュ) 先程同様ビルド履歴を確認します。 main ブランチへのマージとなるため、今回のビルドでは terraform apply まで実行されていることがわかります。 # ログ出力 (重要な部分のみ抜粋) starting build "625f1b9e-0446-4c07-93ef-efaa11ec5310" Starting Step #0 - "tf init" Step #0 - "tf init": Initializing the backend... Step #0 - "tf init": Step #0 - "tf init": Successfully configured the backend "gcs"! Terraform will automatically Step #0 - "tf init": use this backend unless the backend configuration changes. Step #0 - "tf init": Upgrading modules... Step #0 - "tf init": - cicd_test in modules/cloud_storage Step #0 - "tf init": Step #0 - "tf init": Initializing provider plugins... Step #0 - "tf init": - Finding hashicorp/google versions matching "~> 4.65.2"... Step #0 - "tf init": - Installing hashicorp/google v4.65.2... Step #0 - "tf init": - Installed hashicorp/google v4.65.2 (signed by HashiCorp) Step #0 - "tf init": Step #0 - "tf init": Terraform has been successfully initialized! Step #0 - "tf init": Step #0 - "tf init": You may now begin working with Terraform. Try running "terraform plan" to see Step #0 - "tf init": any changes that are required for your infrastructure. All Terraform commands Step #0 - "tf init": should now work. Step #0 - "tf init": Step #0 - "tf init": If you ever set or change modules or backend configuration for Terraform, Step #0 - "tf init": rerun this command to reinitialize your working directory. If you forget, other Step #0 - "tf init": commands will detect it and remind you to do so if necessary. Finished Step #0 - "tf init" Starting Step #1 - "tf plan" Step #1 - "tf plan": Already have image: hashicorp/terraform:1.5.0 Step #1 - "tf plan": Step #1 - "tf plan": Terraform used the selected providers to generate the following execution Step #1 - "tf plan": plan. Resource actions are indicated with the following symbols: Step #1 - "tf plan": + create Step #1 - "tf plan": Step #1 - "tf plan": Terraform will perform the following actions: Step #1 - "tf plan": Step #1 - "tf plan": # module.cicd_test.google_storage_bucket.buckets["cicd-testbucket-01"] will be created Step #1 - "tf plan": + resource "google_storage_bucket" "buckets" { Step #1 - "tf plan": + force_destroy = false Step #1 - "tf plan": + id = (known after apply) Step #1 - "tf plan": + location = "ASIA-NORTHEAST1" Step #1 - "tf plan": + name = "cicd-testbucket-01" Step #1 - "tf plan": + project = "sample-project" Step #1 - "tf plan": + public_access_prevention = (known after apply) Step #1 - "tf plan": + self_link = (known after apply) Step #1 - "tf plan": + storage_class = "STANDARD" Step #1 - "tf plan": + uniform_bucket_level_access = true Step #1 - "tf plan": + url = (known after apply) Step #1 - "tf plan": } Step #1 - "tf plan": Step #1 - "tf plan": Plan: 1 to add, 0 to change, 0 to destroy. Step #1 - "tf plan": Step #1 - "tf plan": ───────────────────────────────────────────────────────────────────────────── Step #1 - "tf plan": Step #1 - "tf plan": Note: You didn't use the -out option to save this plan, so Terraform can't Step #1 - "tf plan": guarantee to take exactly these actions if you run "terraform apply" now. Finished Step #1 - "tf plan" Starting Step #2 - "tf apply" Step #2 - "tf apply": Already have image: hashicorp/terraform:1.5.0 Step #2 - "tf apply": Step #2 - "tf apply": Terraform used the selected providers to generate the following execution Step #2 - "tf apply": plan. Resource actions are indicated with the following symbols: Step #2 - "tf apply": + create Step #2 - "tf apply": Step #2 - "tf apply": Terraform will perform the following actions: Step #2 - "tf apply": Step #2 - "tf apply": # module.cicd_test.google_storage_bucket.buckets["cicd-testbucket-01"] will be created Step #2 - "tf apply": + resource "google_storage_bucket" "buckets" { Step #2 - "tf apply": + force_destroy = false Step #2 - "tf apply": + id = (known after apply) Step #2 - "tf apply": + location = "ASIA-NORTHEAST1" Step #2 - "tf apply": + name = "cicd-testbucket-01" Step #2 - "tf apply": + project = "sample-project" Step #2 - "tf apply": + public_access_prevention = (known after apply) Step #2 - "tf apply": + self_link = (known after apply) Step #2 - "tf apply": + storage_class = "STANDARD" Step #2 - "tf apply": + uniform_bucket_level_access = true Step #2 - "tf apply": + url = (known after apply) Step #2 - "tf apply": } Step #2 - "tf apply": Step #2 - "tf apply": Plan: 1 to add, 0 to change, 0 to destroy. Step #2 - "tf apply": module.cicd_test.google_storage_bucket.buckets["cicd-testbucket-01"]: Creating... Step #2 - "tf apply": module.cicd_test.google_storage_bucket.buckets["cicd-testbucket-01"]: Creation complete after 2s [id=cicd-testbucket-01] Step #2 - "tf apply": Step #2 - "tf apply": Apply complete! Resources: 1 added, 0 changed, 0 destroyed. Finished Step #2 - "tf apply" PUSH DONE Step #2 - "tf apply": Cloud コンソール上でもストレージバケットが作成されています。 Cloud Build × Terraform でストレージバケットが作成された 関連記事 過去の記事では GitHub Actions を用いて今回同様のアーキテクチャの実装方法もご紹介していますのでこちらも是非ご参照ください。 blog.g-gen.co.jp blog.g-gen.co.jp 武井 祐介 (記事一覧) 2022年4月入社 / クラウドソリューション部 / 技術2課所属 趣味はゴルフにロードバイク。IaC や CI/CD 周りのサービスやプロダクトが興味分野です。 Google Cloud 認定全冠達成!(2023年6月)
G-gen の杉村です。Google Cloud 上のネットワークに対する可視性を高めるための管理ツールである Network Intelligence Center を紹介します。Network Intelligence Center は、パケットの到達性テストや、トポロジの可視化、パフォーマンスの可視化、ファイアウォールルール最適化など、ネットワーク管理者向けの複数のツールを備えたサービスです。 Network Intelligence Center とは 接続テスト 概要 分析の仕組み 対応ノード ライブデータプレーン分析 料金 ネットワークトポロジ 概要 エンティティ データの鮮度 料金 パフォーマンスダッシュボード 概要 プロジェクトのパフォーマンスビュー Google Cloud のパフォーマンスビュー 確認可能な指標 料金 ファイアウォールインサイト 概要 シャドウルール 制限が過度に緩いルール ヒットのある拒否ルール 料金 ネットワークアナライザ 概要 VPC ネットワーク ネットワークサービス Google Kubernetes Engine(GKE) ハイブリッド接続 マネージドサービス 料金 Flow Analyzer Network Intelligence Center とは Network Intelligence Center は、Google Cloud におけるネットワークの可視性向上やトラブルシューティングのための、ネットワーク管理者向けツール群です。主に Google Cloud コンソール上で利用します。 参考 : Network Intelligence Center の概要 Network Intelligence Center は以下のモジュール(機能)で構成されています。 No 名称 概要 1 接続テスト ネットワーク的な到達性をテスト 2 ネットワークトポロジ VPC や周辺のオンプレミスネットワークの構成を可視化 3 パフォーマンスダッシュボード ネットワーク基盤に起因するパフォーマンス情報を可視化 4 ファイアウォールインサイト ファイアウォールの不要なルール等を精査 5 ネットワークアナライザ ネットワーク周りの構成ミスや最適でない構成を検出 5 Flow Analyzer VPC Flow Logs を簡単な操作で分析 料金は各モジュールごとに設定されており、基本的には使用ボリュームに応じた従量課金です。 参考 : Network Intelligence Center pricing 接続テスト 概要 接続テスト (Connectivity Test)は、ノード間の通信の到達性をテストできるツールです。Google Cloud でのパケット到達性のトラブルシューティングに活用できます。 テストの際に実際にパケットが送信されるのか、机上シミュレートだけなのかは、接続シナリオによって異なります。実際にパケットを送信して分析する機能は ライブデータプレーン分析 と呼ばれます。 接続元の VM 等と、接続先の VM 等を指定して実行することで、ファイアウォールの設定やルーティングの設定などを論理的に診断し、通信の可否を表示してくれます。通信が到達できない場合は、その理由やどこでパケットが破棄されているかも確認できます。 参考 : 接続テストの概要 接続テスト 分析の仕組み ある VM が別の VM や、オンプレミスのノード等に到達するまで(あるいはその逆方向の通信)には、VPC ネットワークピアリングや Cloud VPN などのネットワーク経路や、VPC ルートテーブルや VPC ファイアウォールなどの通信制御機能を経由します。 接続テスト機能ではこういった様々なネットワーク経路や通信制御機能を考慮に入れた検証が行われます。例えば VPC ファイアウォールで通信がブロックされている場合は、その旨を表示してくれます。 また通信プロトコルとして TCP、UDP、ICMP のほか AH、ESP なども指定できます。 基本的には、クラウドリソース上の設定を読み取った論理的なシミュレーションによって診断が行われますが、後述のとおり、一部のノード間の通信の場合は、実際にパケットを送受信するライブデータプレーン分析が行われます。 対応ノード 接続テスト機能は、例として以下のノード間の通信をテストできます。 Compute Engine VM App Engine(Standard environment のみ) Cloud Run Cloud Run functions Cloud SQL インスタンス Google Kubernetes Engine(GKE) オンプレミスノード(宛先として) インターネット上の IP アドレス(宛先として) また接続テストは、VPC ネットワークピアリングや Network Connectivity Center(NCC)、Cloud VPN や Cloud Interconnect といった各種ネットワーキングサービスに対応しています。 反対に、以下はテストに 対応していない (テスト時に考慮されない)ので注意が必要です(一部抜粋)。 Cloud Armor ポリシー GKE network policies、IP masquerading App Engine(Flexible environment) どのようなネットワークサービスや構成が接続テストに対応しているかの詳細は、以下のドキュメントを参照してください。 参考 : 接続テストの概要 - サポートされている構成 参考 : 接続テストの概要 - サポートされていない構成 ライブデータプレーン分析 論理的なシミュレーションだけでなく、実際にパケットを送信して分析する機能は ライブデータプレーン分析 と呼ばれます。ただし、この機能はあくまでネットワーク到達性の診断のために使うものであって、継続的なモニタリング目的に用いるものではないことに留意して下さい。 ライブデータプレーン分析は、以下のノード間の TCP および UDP 通信のテストの際に行われます。 送信元 Compute Engine VM サーバーレス VPC アクセスコネクタを使う Cloud Run、Cloud Run functions、App Engine(Standard environment) Cloud SQL インスタンス Google Kubernetes Engine(GKE)コントロールプレーン 宛先 Compute Engine VM Internal passthrough Network Load Balancer インターネット IP アドレス(エッジロケーション間との通信をテスト) Cloud Interconnect 以下のプライベートエンドポイント Cloud SQL Memorystore for Redis Google Kubernetes Engine(GKE)コントロールプレーン また、対応しているネットワークリソースにいくつかの制限があります。詳細は以下のドキュメントを参照してください。 参考 : 接続テストの概要 - 接続テストによるライブ データプレーンの分析方法 料金 1か月の中で実行されたテスト回数に対して課金されます。月間20回までは無料で、21回目からは1回あたり $0.15 の料金が発生します。 参考 : Network Intelligence Center pricing - Connectivity Tests pricing details ネットワークトポロジ 概要 ネットワークトポロジ (Network Topology)は、Google Cloud の VPC を中心としたネットワーク構造とパフォーマンスを可視化するためのツールです。 Google Cloud 基盤からリアルタイムにデータを収集し、複数プロジェクトにまたがる構成も可視化できます。エージェント等は不要です。過去6週間の履歴が保存されます。 この機能により、ネットワーク構成をグラフィカルに把握できる他、ノード間のトラフィック量が表示されるため、パフォーマンス分析に活かすことも可能です。 ネットワークトポロジのグラフ表示 参考 : ネットワーク トポロジの概要 エンティティ ネットワークトポロジ機能における エンティティ とは、通常のネットワーク用語における「ノード」とほぼ同様です。ネットワーク通信を行うリソースの個々の単位を表します。エンティティはグラフ上に、特有のアイコンで表現されます。例として、以下のようなものがあります。 VM インスタンス VM インスタンスグループ ロードバランサー Cloud NAT VPC ネットワークピアリング 国(通信元クライアントの存在する国) Cloud Interconnect Cloud VPN オンプレミス(他のパブリッククラウドもこの表記) アイコン画像の凡例は、以下のドキュメントを参照してください。 参考 : ネットワーク トポロジの概要 - エンティティ データの鮮度 表示されるデータは、毎時0分を起点とする1時間ごとのスナップショットです。 Google Cloud コンソール上では、コントロールバーをスライドさせて特定の時刻のグラフを表示させます。 過去データは6週間分まで保存されます。 料金 当機能には、以下の料金体系が設定されていながら、ネットワークトポロジ機能の料金は 2026年2月現在、無償 で利用できます。公式の料金ページには「すべてのユーザーが100%割引で利用可能。課金開始の90日前には通知する」旨の記載がされています。 ネットワークトポロジ機能の料金は、後述のパフォーマンスダッシュボードの料金とセットです。 ネットワークトポロジとパフォーマンスダッシュボードを有効化すると、VM 数に応じた料金($0.0011 / VM / 時間)が発生することに加え、VM とインターネット間で通信が生じた場合は追加の料金($0.0008 / VM / 時間)が発生します。 VM が20台の場合、30日ある月では $0.0011 × 20台 × 24時間 × 30日 で $15.84/月 が発生し、これら全ての VM が常にインターネットと通信していたと仮定すると、追加で $0.0008 × 20台 × 24時間 × 30日 = $11.52/月 が発生します。 参考 : Network Intelligence Center pricing - Network Topology and Performance Dashboard pricing details パフォーマンスダッシュボード 概要 パフォーマンスダッシュボード 機能では、Google Cloud ネットワークのパフォーマンスを可視化できます。アプリケーション固有の問題と、Google Cloud ネットワーク基盤の問題を切り分けすることにも役立てることができます。 また収集されたデータは Cloud Monitoring にもエクスポートされます。データは過去6週間分まで保存されています。 パフォーマンスダッシュボードでは大きく分けて プロジェクトのパフォーマンスビュー と Google Cloud のパフォーマンスビュー の2種類が閲覧可能です。 参考 : パフォーマンス ダッシュボードの概要 パフォーマンスダッシュボード プロジェクトのパフォーマンスビュー プロジェクトのパフォーマンスビュー では「VM インスタンス間のトラフィック」と「Google Cloud とインターネットロケーション間のトラフィック」の2種類のパフォーマンスが確認できます。 「VM インスタンス間のトラフィック」では、別々のゾーンにある Compute Engine VM 同士の通信におけるパケットロスとレイテンシの指標が表示可能です。 「Google Cloud とインターネットロケーション間のトラフィック」では、Compute Engine VM が存在するリージョンと、その VM が通信するインターネット上のノードの間のレイテンシが表示できます。 Google Cloud のパフォーマンスビュー Google Cloud のパフォーマンスビュー では特定プロジェクトに依存しない、Google Cloud 全体としての指標が確認できます。 プロジェクトのパフォーマンスビューと同様に「VM インスタンス間のトラフィック」と「Google Cloud とインターネットロケーション間のトラフィック」の2種類のパフォーマンスが確認でき、プロジェクトで観測されたパフォーマンスとの比較が可能です。 ネットワーク関連のパフォーマンス低下が起こった際に、その問題がプロジェクト固有 (あるいは Google Cloud 物理基盤のごく一部) に発生した問題なのか、Google Cloud 基盤全体で発生している問題なのかの把握に役立ちます。 また、Google Cloud 上に新しいインフラを構築しようとしているときの設計にも役立ちます。予め、特定のリージョン間・ゾーン間の平均的なレイテンシを確認できるため、VM をわざわざ構築して検証しなくても、過去の実績に基づいた情報を確認できます。 ゾーン間のレイテンシの実績が分かる 確認可能な指標 各ビューでは 指標 (メトリクス)として パケットロス と レイテンシ が確認できます。 バックエンドの仕組みとして、Google Cloud の VM をホストしている物理マシン上でワーカーが実行されており、このワーカー間で検査用パケットがやりとりされ、指標が計測されています。我々ユーザは、このワーカーやパケットを意識することはありませんし、VM のパフォーマンスに影響はありません。 料金 パフォーマンスダッシュボードの料金は前述のネットワークトポロジ機能とセットです。 前述のとおり、当機能には詳細に料金体系が設定されていながら、 2026年2月現在、無償 で利用できます。公式の料金ページには「すべてのユーザーが100%割引で利用可能。課金開始の90日前には通知する」旨の記載がされています。 参考 : Network Intelligence Center pricing - Network Topology and Performance Dashboard pricing details ファイアウォールインサイト 概要 ファイアウォールインサイト はファイアウォールルールの最適化と整理に役立つツールです。緩すぎるルールに対するサジェストや、逆に無駄なルールの削除の推奨などを表示します。 当機能は、ファイアウォールが意図したとおりに使われていることを確認したり、あるいはルールへのヒット数へのスパイクを検知してアラート発報するなどの用途にも利用可能です。 当機能で観測可能な事項は主に「 シャドウルール 」「 制限が過度に緩いルール 」「 ヒットのある拒否ルール 」に大別できます。 このうち後者の2つはファイアウォールルールのログから情報を取得しています。そのためログが有効になっていないファイアウォールルールは検査対象になりません。 参考 : ファイアウォール インサイトの概要 ファイアウォールインサイト なお、当機能のバックエンドでは Recommender API が使われています。 参考 : インサイト シャドウルール シャドウルール (Shadowed rules)とは「より優先度の高い他のルールによってカバー済みのため、存在している意味がないルール」を意味します。 例えば「10.10.0.0/16 からの tcp:80 パケットは許可(優先度500)」というルールが存在するとします。同じ VPC に「10.10.0.0/24 からの tcp:80 パケットは許可(優先度1000)」というルールがあるとします。後者のルールが適用され得るパケットは、前者のルールによって先に許可されるため、後者のルールが使われることは決してありません。このようなルールが、シャドウルールと呼ばれます。 このようなルールは構成情報を分かりづらくなるもとであり、適切な設計を阻害したり、運用性を低下させるため、削除することが望ましいです。 ファイアウォールインサイトでは VPC ファイアウォールルールとファイアウォールポリシーの両方でこのようなシャドウルールを検知することができます。 VPC ファイアウォールルールとファイアウォールポリシーの違いについては、以下の記事も参照してください。 blog.g-gen.co.jp 制限が過度に緩いルール 制限が過度に緩いルール (Overly permissive rules)とはファイアウォールルールのうち、利用実態から判断して過剰な許可条件が設定されており緩すぎると判断される許可ルール(allow rules)を指します。具体的には、以下のような許可ルールです。 名称 概要 ヒットのない許可ルール 観察期間中にヒットがなかった。今後ヒットの可能性があるかどうか機械学習での予測が表示される (同組織内の類似ルールから予測) 傾向分析に基づいて使用されていない許可ルール 過去6週間の使用傾向に基づいた機械学習により、今後の使用可能性が低いルールを表示 未使用の属性を含む許可ルール 観察期間中にヒットしなかった IP アドレスやポート範囲などの属性を含む許可ルールを表示 制限が過度に緩い IP アドレスまたはポート範囲を含む許可ルール 広すぎる IP アドレスまたはポート範囲が存在する可能性がある許可ルールを特定 このように、機械学習に基づいて不用意なルール設定がされている許可ルールを特定することにより、許可ルールの設定を最小限に抑える判断の補助に利用できます。ただしいずれも、検知にはファイアウォールルールで ロギングがオン になっている必要があります。 VPC ファイアウォールのルールとファイアウォールポリシーのルールの両方が検査対象になります。 ヒットのある拒否ルール ヒットのある拒否ルール (Deny rules with hits)では指定した観察期間中に deny ルールにどのくらいのパケットがヒットしたかを確認できます。 ネットワークの構成ミスや、意図的な攻撃による deny ヒットの急増などを検知することができます。 ただし検知にはファイアウォールルールでロギングがオンになっている必要があります。 料金 シャドウルールの検知では、プロジェクトで機能を有効化した直後の最初の検査で、プロジェクトに存在するファイアウォールルールあたり $1 が発生します。その後、追加の検査では、1ルールあたり$0.1が発生します。検査は、ファイアウォールルールに変更が加わった日のみ、VPC ネットワークごとに行われます。 制限が過度に緩いルールの検査は、検査されたファイアウォールログのログエントリの数量に対して課金されます。100万〜100億(1M〜10,000M)エントリまでは $0.20/百万ログエントリ/月の料金が発生し、それ以上のログ量ではティア状に単価が安くなるように設定されています。 ヒットのある拒否ルールや、その他のファイアウォールインサイト関連のメトリクスは無料です。 なお、料金が発生するため、上記の各検知機能は個別にオン・オフができるようになっています。 参考 : Network Intelligence Center pricing - Firewall Insights pricing details ネットワークアナライザ 概要 ネットワークアナライザ とは、VPC ネットワークの構成ミスや最適でない構成を検出するためのツールです。 検知事項の重大度は重大>高>中>低として分類され、以下のカテゴリ分けがされます。 VPC ネットワーク ネットワークサービス Google Kubernetes Engine(GKE) ハイブリッド接続 マネージドサービス 分析は、関連する設定が変更されることをトリガにして約10分後に実行されます。また、少なくとも1日1回の定期分析も行われます。 参考 : ネットワーク アナライザの概要 ネットワークアナライザ VPC ネットワーク VPC ネットワーク関連の検知事項には、以下のようなものがあります。 名称 概要 無効なネクストホップを含むルートに関する分析情報 ネクストホップとなっていた VM 等が利用不可でルートが無効になっている IP アドレス使用状況の分析情報 サブネット内の空き IP が残り少ない 未使用 IP アドレスの分析情報 プロジェクトで 24 時間以上割り振られていない外部 IP アドレスがある ネットワークサービス ネットワークサービス関連の検知事項には、以下のようなものがあります。 ロードバランサのヘルスチェック用のファイアウォールが構成されていない ロードバランサのヘルスチェック用の IP アドレス範囲がブロックされている ロードバランサで実トラフィックとヘルスチェックで異なるポートを使っている Cloud NAT でリソース不足によるパケットドロップが起きている Google Kubernetes Engine(GKE) Google Kubernetes Engine(GKE)の検知事項には、以下のようなものがあります。 ルーティングの問題により、GKE ノードからコントロール プレーンへの接続がブロックされる Pod に割り当てられたアドレス範囲の使用率が 80% を超えている その他、GKE のネットワーク構成がベストプラクティスに従っていない ハイブリッド接続 ハイブリッド接続の検知事項には、以下のようなものがあります。 Cloud Router や VPC ネットワークピリング経由で VPC が受け取った動的ルートが、より優先度の高い静的ルートやサブネットルートによりシャドウルート化してしまっている マネージドサービス マネージドサービスの検知事項には、以下のようなものがあります。 下り(外向き)ファイアウォールによって Cloud SQL インスタンスへの接続がブロックされる ルーティングの問題によって Cloud SQL インスタンスへの接続がブロックされる Cloud SQL インスタンスへの接続に関する問題: インスタンスが実行されていない 料金 ネットワークアナライザ機能も、ネットワークトポロジ機能、パフォーマンスダッシュボード機能と同様に、詳細に料金体系が設定されていながら、2026年2月現在、無償で利用できます。公式の料金ページには「すべてのユーザーが100%割引で利用可能。課金開始の90日前には通知する」旨の記載がされています。 ネットワークアナライザの料金は、有効化したプロジェクト内で動作する Compute Engine VM 数 および Google Kubernetes Engine(GKE)ノード数によって決定します。$0.0011 / VM / 時間の料金が設定されています。 参考 : Network Intelligence Center pricing - Network Analyzer pricing details Flow Analyzer Flow Analyzer は、VPC Flow Logs を分析するためのツールです。従来、VPC Flow Logs を分析するには、BigQuery にエクスポートするか、Cloud Logging の Log Analytics 機能を用いて、複雑な SQL を記述して検索を実行する必要がありました。Flow Analyzer を使うと、VPC Flow Logs に対してより簡単にクエリを自動作成したり、ビジュアライズすることができます。 参考 : Flow Analyzer の概要 なお、Flow Analyzer のバックエンドでは Cloud Logging の Log Analytics 機能が使われています。 Flow Analyzer は、ログバケットに保存されている VPC Flow Logs に対してクエリを実行します。これにより、VPC ネットワーク内を通過して Compute Engine VM や GKE ノードに出入りしたパケットの情報をクエリできます。 クエリ結果は、データ量またはレイテンシ(RTT、Round-trip time の傾向を表示)で表示できます。 杉村 勇馬 (記事一覧) 執行役員 CTO 元警察官という経歴を持つ IT エンジニア。クラウド管理・運用やネットワークに知見。AWS 認定資格および Google Cloud 認定資格はすべて取得。X(旧 Twitter)では Google Cloud や Google Workspace のアップデート情報をつぶやいています。 Follow @y_sugi_it
Google Cloud (旧称 GCP) の事前トレーニング済みの API のひとつである Cloud Vision API を用いて車のナンバープレートをマスキングする処理をご紹介します。 はじめに Vision AI Vision API 事前確認でわかったこと 構成図 準備 ディレクトリ構成 main.tf gcf_source_code/detect_car main.py requirements.txt gcf_source_code/detect_license_plate main.py requirements.txt 動作検証 検証データ 実行 Cloud Vision API と Cloud Functions でナンバープレートをマスキング はじめに Vision AI Vision AI とは、Google Cloud 上で画像や動画から分析情報を取得することができる以下の 3 つのプロダクトの総称です。 Vertex AI Vision Custom ML models ( AutoML or Vertex AI 独自モデル) Vision API 今回は、その中の Vision API を使用します。 Vision API Vision API とは、事前トレーニング済み Vision API モデル使用して、オブジェクトの検知や OCR などが行なえます。 Vision API には、主に以下のような機能があります。 No 機能タイプ 説明 1 Text detection 画像の光学式文字認識 (OCR)。画像内の UTF-8 テキストを識別し抽出できる。 2 Landmark detection ランドマークの名前、信頼度スコア、および境界ボックス (画像内の位置) が取得できる。 3 Label detection 「People」や「Car」のような一般化されたラベルが取得でき、各ラベルには説明と信頼度スコアが記載されている。 4 Object localization 複数のオブジェクトに「People」や「Car」のような一般的なラベルと境界ボックス (画像内の位置) が取得できる。 5 Face detection 顔を特定し、目、耳、鼻、口などの特定の顔の「ランドマーク」を信頼度スコアとともに取得でき、また表情から感情の尤度評価も取得できる。 その他の機能、また詳細については以下をご参照下さい。 Features list また、 Cloud Vision API の概要ページ からローカルの画像を Vision API に読み込ませ、どのような出力がでるかすぐに確認できます。 (※ 当記事で扱う車の画像データに含まれるナンバープレートは、個人情報保護の観点からモザイク処理をかけております。) Cloud Vision API 概要ページの出力結果 Vision API は事前トレーニング済み Vision API モデルで解決できる場合に有用ですが、自社の製品をカスタムラベルとして設定したい等、別途トレーニングが必要な場合は Vertex AI Vision や AutoML などを用いる必要があります。 事前確認でわかったこと 事前に 2 枚の画像を、Cloud Vision API の概要ページからどのような出力結果がえられるのか試してみます。 出力結果① 出力結果② 出力結果①では、検出されたオブジェクトの中に Car (車)と License Plate (ナンバープレート) が含まれていることが確認できます。 しかし、出力結果②では Car は検出できているが License Plate が検出できていない ことがわかります。 そこで、出力結果②の画像から車周辺を切り取りった新たな画像で確認してみます。 出力結果③ 車周辺を切り取りった画像の出力 (出力結果③) では、License Plate がオブジェクトとして検出されました。 これらの事前確認結果から、Cloud Vision API へリクエストを送信する際、画像いっぱいに車体を入れることでナンバープレートを検出してくれる可能性が高まることがわかりました。 そこで今回は、画像内の車を検出したら車周辺を切り取ったものを新しい画像とし、その新しい画像からナンバープレートを検出しマスキング処理 (白塗り) を行う構成とします。 ナンバープレートをマスキングする処理の流れ 構成図 今回の構成図は以下のとおりです。 構成図 ユーザーが Raw Data バケットに画像データをアップロードすると、Cloud Storage トリガー経由で Cloud Functions (車検出&抽出関数) が起動します。画像内の車オブジェクトがあれば車周辺を切り取った新しい画像を Detected Car バケットに格納します。 次に、Detected Car バケットに画像データがアップロードされると、Cloud Storage トリガー経由で Cloud Functions (LP検出&マスキング関数) が起動します。画像内のナンバープレートオブジェクトがあればナンバープレートをマスキングした新しい画像を Detected LP バケットに格納します。 準備 ディレクトリ構成 開発環境は Cloud Shell を用いて行います。ディレクトリ構造は以下のとおりです。 terraform ディレクトリ配下は、以下のとおりです。 terraform |-- gcf_source_code | |-- detect_car | | |-- main.py | | `-- requirements.txt | ` -- detect_license_plate | |-- main.py | `-- requirements.txt ` -- main.tf main.tf main.tf には Terraform のコードを記述しています。 locals { terraform_service_account = ${Terraform 実行に使われるサービスアカウントのメールアドレス} project_name = ${プロジェクト名} project_id = ${プロジェクト ID} folder_id = ${フォルダ ID} billing_account_id = ${請求先アカウント ID} } # terraform & provider の設定 terraform { required_providers { google = { source = "hashicorp/google" version = ">= 4.0.0" } } required_version = ">= 1.3.0" backend "gcs" { bucket = ${tfstate ファイルを格納する Cloud Storage バケット名} impersonate_service_account = ${Terraform 実行に使われるサービスアカウントのメールアドレス} } } # サービスアカウント権限借用の設定 provider "google" { alias = "impersonation" scopes = [ "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", ] } data "google_service_account_access_token" "default" { provider = google.impersonation target_service_account = local.terraform_service_account scopes = ["userinfo-email", "cloud-platform"] lifetime = "1200s" } # Google プロバイダの設定 provider "google" { project = local.project_id region = "asia-northeast1" access_token = data.google_service_account_access_token.default.access_token request_timeout = "60s" } ###################################### ### プロジェクトの作成と API の有効化 ### ###################################### # プロジェクトの作成 resource "google_project" "poc" { name = local.project_name project_id = local.project_id folder_id = local.folder_id billing_account = local.billing_account_id } # API の有効化 module "tenant_a_project_services" { source = "terraform-google-modules/project-factory/google//modules/project_services" version = "14.2.1" project_id = google_project.poc.project_id enable_apis = true activate_apis = [ "iam.googleapis.com", "cloudbuild.googleapis.com", "run.googleapis.com", "cloudfunctions.googleapis.com", "pubsub.googleapis.com", "eventarc.googleapis.com", "artifactregistry.googleapis.com", "storage.googleapis.com", "vision.googleapis.com" ] disable_services_on_destroy = false } ####################################### ### サービスアカウントの作成と権限の付与 ## ####################################### # Cloud Functions 用サービスアカウントの作成と権限付与 resource "google_service_account" "sa_gcf" { project = google_project.poc.project_id account_id = "sa-gcf" display_name = "Cloud Functions 用サービスアカウント" } resource "google_project_iam_member" "invoke_gcf" { project = google_project.poc.project_id role = "roles/run.invoker" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_project_iam_member" "storage_admin" { project = google_project.poc.project_id role = "roles/storage.admin" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_project_iam_member" "event_receiving" { project = google_project.poc.project_id role = "roles/eventarc.eventReceiver" member = "serviceAccount:${google_service_account.sa_gcf.email}" depends_on = [google_project_iam_member.invoke_gcf] } resource "google_project_iam_member" "artifactregistry_reader" { project = google_project.poc.project_id role = "roles/artifactregistry.reader" member = "serviceAccount:${google_service_account.sa_gcf.email}" depends_on = [google_project_iam_member.event_receiving] } # Eventarc のサービスアカウントに権限付与 resource "google_project_iam_member" "serviceAccount_token_creator" { project = google_project.poc.project_id role = "roles/iam.serviceAccountTokenCreator" member = "serviceAccount:service-${google_project.poc.number}@gcp-sa-pubsub.iam.gserviceaccount.com" depends_on = [module.tenant_a_project_services] } # Cloud Storage のサービスアカウントに権限付与 data "google_storage_project_service_account" "gcs_account" { project = google_project.poc.project_id depends_on = [ module.tenant_a_project_services ] } resource "google_project_iam_member" "gcs_pubsub_publishing" { project = google_project.poc.project_id role = "roles/pubsub.publisher" member = "serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}" } ################################ ### バケットとオブジェクトの作成 ### ################################ # Cloud Functions のソースコード格納用バケットの作成 resource "google_storage_bucket" "source_gcf" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-source-gcf" force_destroy = true } # Cloud Functions で使うソースコードを ZIP 化 data "archive_file" "detect_car" { type = "zip" source_dir = "./gcf_source_code/detect_car" output_path = "./zip_source_code/detect_car.zip" } data "archive_file" "detect_license_plate" { type = "zip" source_dir = "./gcf_source_code/detect_license_plate" output_path = "./zip_source_code/detect_license_plate.zip" } # ZIP 化したソースコードをバケットに追加 resource "google_storage_bucket_object" "detect_car" { name = "detect-car.${data.archive_file.detect_car.output_md5}.zip" bucket = google_storage_bucket.source_gcf.name source = data.archive_file.detect_car.output_path } resource "google_storage_bucket_object" "detect_license_plate" { name = "detect-license-plate.${data.archive_file.detect_license_plate.output_md5}.zip" bucket = google_storage_bucket.source_gcf.name source = data.archive_file.detect_license_plate.output_path } # raw_data バケットの作成 resource "google_storage_bucket" "raw_data" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-raw-data" force_destroy = true } # detected_car バケットの作成 resource "google_storage_bucket" "detected_car" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-detected-car" force_destroy = true } # not_detected_car バケットの作成 resource "google_storage_bucket" "not_detected_car" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-not-detected-car" force_destroy = true } # detected_license_plate バケットの作成 resource "google_storage_bucket" "detected_license_plate" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-detected-license-plate" force_destroy = true } # not_detected_license_plate バケットの作成 resource "google_storage_bucket" "not_detected_license_plate" { project = google_project.poc.project_id location = "asia-northeast1" name = "${google_project.poc.project_id}-not-detected-license-plate" force_destroy = true } ############################ ### Cloud Functions 作成 ### ############################ # detect_car 関数 resource "google_cloudfunctions2_function" "detect_car" { depends_on = [ google_project_iam_member.event_receiving, google_project_iam_member.artifactregistry_reader, google_project_iam_member.serviceAccount_token_creator ] name = "detect-car" location = "asia-northeast1" description = "画像から車を切り取って detect_car バケットに格納する関数" build_config { runtime = "python310" entry_point = "main" # Set the entry point in the code source { storage_source { bucket = google_storage_bucket.source_gcf.name object = google_storage_bucket_object.detect_car.name } } } service_config { max_instance_count = 3 min_instance_count = 1 available_memory = "256M" timeout_seconds = 60 environment_variables = { DETECTED_CAR_BUCHET_NAME = google_storage_bucket.detected_car.name NOT_DETECTED_CAR_BUCHET_NAME = google_storage_bucket.not_detected_car.name KEY = "Car" } service_account_email = google_service_account.sa_gcf.email } event_trigger { trigger_region = "asia-northeast1" event_type = "google.cloud.storage.object.v1.finalized" retry_policy = "RETRY_POLICY_RETRY" service_account_email = google_service_account.sa_gcf.email event_filters { attribute = "bucket" value = google_storage_bucket.raw_data.name } } } # detected_license_plate 関数 resource "google_cloudfunctions2_function" "detected_license_plate" { depends_on = [ google_project_iam_member.event_receiving, google_project_iam_member.artifactregistry_reader, google_project_iam_member.serviceAccount_token_creator ] name = "detected-license-plate" location = "asia-northeast1" description = "画像からナンバープレートをマスキングして detected_license_plate バケットに格納する関数" build_config { runtime = "python310" entry_point = "main" # Set the entry point in the code source { storage_source { bucket = google_storage_bucket.source_gcf.name object = google_storage_bucket_object.detect_license_plate.name } } } service_config { max_instance_count = 3 min_instance_count = 1 available_memory = "256M" timeout_seconds = 60 environment_variables = { DETECTED_LICENSE_PLATE = google_storage_bucket.detected_license_plate.name NOT_DETECTED_LICENSE_PLATE = google_storage_bucket.not_detected_license_plate.name KEY = "License plate" } service_account_email = google_service_account.sa_gcf.email } event_trigger { trigger_region = "asia-northeast1" event_type = "google.cloud.storage.object.v1.finalized" retry_policy = "RETRY_POLICY_RETRY" service_account_email = google_service_account.sa_gcf.email event_filters { attribute = "bucket" value = google_storage_bucket.detected_car.name } } } gcf_source_code/detect_car main.py gcf_source_code/detect_car には、画像から車を切り取ってバケットに格納する処理を行う Cloud Functions のソースコードを格納しています。 from io import BytesIO import os from cloudevents.http import CloudEvent import functions_framework from google.cloud import vision from google.cloud import storage from PIL import Image DETECTED_CAR_BUCHET_NAME = os.environ.get( "DETECTED_CAR_BUCHET_NAME" ) NOT_DETECTED_CAR_BUCHET_NAME = os.environ.get( "NOT_DETECTED_CAR_BUCHET_NAME" ) KEY = os.environ.get( "KEY" ) # クライアントを初期化 vision_image_annotator_client = vision.ImageAnnotatorClient() storage_client = storage.Client() def download_blob (bucket_name, blob_name): # バケットを取得 bucket = storage_client.get_bucket(bucket_name) # オブジェクト(画像)を取得 blob = bucket.blob(blob_name) image_data = blob.download_as_bytes() return image_data def localize_objects (image_data): # Vision API 実行 image = vision.Image(content=image_data) objects = vision_image_annotator_client.object_localization(image=image).localized_object_annotations # 辞書型に整形 localize_object_di = {} for object_ in objects: vertex_li = [] for vertex in object_.bounding_poly.normalized_vertices: vertex_li.append({ "x" : vertex.x, "y" : vertex.y}) localize_object_di[object_.name] = { "score" : object_.score, "vertex" : vertex_li } return localize_object_di def cropped_image_upload_detected_car_bucket ( image_data, key_di, detected_car_bucket_name, blob_name): # BytesIOを使って画像データを読み込み image = Image.open(BytesIO(image_data)) # 画像のサイズを取得 width, height = image.size # 左下と右上の座標に margin を追加 margin = 0.05 key_di[ 'vertex' ][ 0 ][ "x" ] = key_di[ 'vertex' ][ 0 ][ "x" ] - margin # 左下 x 軸 key_di[ 'vertex' ][ 0 ][ "y" ] = key_di[ 'vertex' ][ 0 ][ "y" ] - margin # 左下 y 軸 key_di[ 'vertex' ][ 2 ][ "x" ] = key_di[ 'vertex' ][ 2 ][ "x" ] + margin # 右上 x 軸 key_di[ 'vertex' ][ 2 ][ "y" ] = key_di[ 'vertex' ][ 2 ][ "y" ] + margin # 右上 y 軸 x_coords = [v[ "x" ] * width for v in key_di[ "vertex" ]] y_coords = [v[ "y" ] * height for v in key_di[ "vertex" ]] # 切り抜きの領域を設定 left = min (x_coords) upper = min (y_coords) right = max (x_coords) lower = max (y_coords) # 画像を切り抜く cropped_image = image.crop((left, upper, right, lower)) # ローカルに一時的に保存 tmp_file = f "/tmp/{blob_name}" cropped_image.save(tmp_file) # Cloud Storage へアップロード bucket = storage_client.get_bucket(detected_car_bucket_name) blob_cropped = bucket.blob(blob_name) blob_cropped.upload_from_filename(tmp_file) # ローカルから削除 os.remove(tmp_file) return "ok" def upload_not_detected_car_bucket (image_data, not_detected_car_bucket_name, blob_name): # BytesIOを使って画像データを読み込み image = Image.open(BytesIO(image_data)) # ローカルに一時的に保存 tmp_file = f "/tmp/{blob_name}" image.save(tmp_file) # Cloud Storage へアップロード bucket = storage_client.get_bucket(not_detected_car_bucket_name) blob_cropped = bucket.blob(blob_name) blob_cropped.upload_from_filename(tmp_file) return "ok" def is_check_key (localize_object_di, key): # 辞書のキーの中に特定の文字列が1つだけあるかどうかをチェック key_to_check = key key_count = list (localize_object_di.keys()).count(key_to_check) if key_count == 1 : return True else : return False @ functions_framework.cloud_event def main (cloud_event: CloudEvent): # CloudEvent から渡されたデータを取得 data = cloud_event.data bucket_name = data[ "bucket" ] file_name = data[ "name" ] image_data = download_blob(bucket_name=bucket_name, blob_name=file_name) localize_object_di = localize_objects(image_data=image_data) if is_check_key(localize_object_di=localize_object_di, key=KEY): cropped_image_upload_detected_car_bucket( image_data=image_data, key_di=localize_object_di[KEY], detected_car_bucket_name=DETECTED_CAR_BUCHET_NAME, blob_name=file_name) else : upload_not_detected_car_bucket( image_data=image_data, not_detected_car_bucket_name=NOT_DETECTED_CAR_BUCHET_NAME, blob_name=file_name) return "ok" requirements.txt functions-framework==3.* cloudevents==1.9.0 google-cloud-vision==3.4.4 google-cloud-storage==2.10.0 numpy==1.25.1 Pillow==10.0.0 opencv-python==4.8.0.74 gcf_source_code/detect_license_plate main.py gcf_source_code/detect_license_plate には、ナンバープレートをマスキングしてバケットに格納する処理を行う Cloud Functions のソースコードを格納しています。 from io import BytesIO import os from cloudevents.http import CloudEvent import functions_framework from google.cloud import vision from google.cloud import storage from PIL import Image from PIL import ImageDraw DETECTED_LICENSE_PLATE = os.environ.get( "DETECTED_LICENSE_PLATE" ) NOT_DETECTED_LICENSE_PLATE = os.environ.get( "NOT_DETECTED_LICENSE_PLATE" ) KEY = os.environ.get( "KEY" ) # クライアントを初期化 vision_image_annotator_client = vision.ImageAnnotatorClient() storage_client = storage.Client() def download_blob (bucket_name, blob_name): # バケットを取得 bucket = storage_client.get_bucket(bucket_name) # オブジェクト(画像)を取得 blob = bucket.blob(blob_name) image_data = blob.download_as_bytes() return image_data def localize_objects (image_data): # Vision API 実行 image = vision.Image(content=image_data) objects = vision_image_annotator_client.object_localization(image=image).localized_object_annotations # 辞書型に整形 localize_object_di = {} for object_ in objects: vertex_li = [] for vertex in object_.bounding_poly.normalized_vertices: vertex_li.append({ "x" : vertex.x, "y" : vertex.y}) localize_object_di[object_.name] = { "score" : object_.score, "vertex" : vertex_li } return localize_object_di def masked_image_upload_detected_license_plate_bucket ( image_data, key_di, detected_license_plate_bucket, blob_name): # BytesIOを使って画像データを読み込み image = Image.open(BytesIO(image_data)) # 画像のサイズを取得 width, height = image.size x_coords = [v[ "x" ] * width for v in key_di[ "vertex" ]] y_coords = [v[ "y" ] * height for v in key_di[ "vertex" ]] # 切り抜きの領域を設定 left = min (x_coords) upper = min (y_coords) right = max (x_coords) lower = max (y_coords) # 画像に白い矩形を描画 ImageDraw.Draw(image).rectangle(((left, upper), (right, lower)), fill= "white" ) # ローカルに一時的に保存 tmp_file = f "/tmp/{blob_name}" image.save(tmp_file) # Cloud Storage へアップロード bucket = storage_client.get_bucket(detected_license_plate_bucket) blob_cropped = bucket.blob(blob_name) blob_cropped.upload_from_filename(tmp_file) # ローカルから削除 os.remove(tmp_file) return "ok" def upload_not_detected_license_plate_bucket (image_data, not_detected_license_plate_bucket, blob_name): # BytesIOを使って画像データを読み込み image = Image.open(BytesIO(image_data)) # ローカルに一時的に保存 tmp_file = f "/tmp/{blob_name}" image.save(tmp_file) # Cloud Storage にアップロード bucket = storage_client.get_bucket(not_detected_license_plate_bucket) blob_cropped = bucket.blob(blob_name) blob_cropped.upload_from_filename(tmp_file) return "ok" def is_check_key (localize_object_di, key): # 辞書のキーの中に特定の文字列が1つだけあるかどうかをチェック key_to_check = key key_count = list (localize_object_di.keys()).count(key_to_check) if key_count == 1 : return True else : return False @ functions_framework.cloud_event def main (cloud_event: CloudEvent): # Cloud Storage から渡されたデータを取得 data = cloud_event.data bucket_name = data[ "bucket" ] file_name = data[ "name" ] image_data = download_blob(bucket_name=bucket_name, blob_name=file_name) localize_object_di = localize_objects(image_data=image_data) if is_check_key(localize_object_di=localize_object_di, key=KEY): masked_image_upload_detected_license_plate_bucket( image_data=image_data, key_di=localize_object_di[KEY], detected_license_plate_bucket=DETECTED_LICENSE_PLATE, blob_name=file_name) else : upload_not_detected_license_plate_bucket( image_data=image_data, not_detected_license_plate_bucket=NOT_DETECTED_LICENSE_PLATE, blob_name=file_name) return "ok" requirements.txt functions-framework==3.* cloudevents==1.9.0 google-cloud-vision==3.4.4 google-cloud-storage==2.10.0 numpy==1.25.1 Pillow==10.0.0 opencv-python==4.8.0.74 動作検証 検証データ 以下の 3 枚の画像データで動作検証を行います。 検証前データ 実行 生データ格納用バケットに検証データをアップロードします。 生データ格納用バケットコンソール画面 直後に Cloud Storage トリガー経由で Cloud Functions が起動し、最終的にナンバープレートマスキング加工後格納バケットにオブジェクトが生成されました。 ナンバープレートマスキング加工後格納バケットコンソール画面 中身は以下のようになってました。 検証後データ 検証前データと比較すると、車周辺部分で切り取られ、ナンバープレートがマスキングできていることを確認できました。 G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
当記事では、BigQuery ML から Vertex AI の基盤モデルを呼び出して感情分析を行う方法を解説します。 前提知識 BigQuery ML Generative AI Support on Vertex AI リモートモデル ML.GENERATE_TEXT 関数 概要 引数 出力 クォータと制限 概要 使用するデータ 今回やること プロンプト設計 精度結果 準備 API の有効化 データセットの作成 Connection の作成 サービスアカウントに権限付与 リモートモデルの作成 実行 感情分析を実行 評価 前提条件 実行 Use Generative AI from BigQuery 前提知識 BigQuery ML 以下の記事では BigQuery ML を詳細に解説していますので、ご参照ください。 blog.g-gen.co.jp Generative AI Support on Vertex AI 先日 Vertex AI でも Generative AI がサポートされました。Generative AI モデル (基盤モデル) の裏側は PaLM 2 が利用されており、多言語、推論、コーディング機能が強化された最先端の大規模言語モデル (LLM) です。 Vertex AI の Generative AI サポートについての詳細は以下の記事をご参照下さい。 blog.g-gen.co.jp リモートモデル リモートモデル とは、Vertex AI エンドポイントまたはリモートサービスタイプを BigQuery から呼び出すことができる機能です。 リモートサービスタイプでは、2023 年 7 月現在、以下のサービスをサポートしています。 Vertex AI の 基盤モデル Cloud Natural Language API Cloud Translation API Cloud Vison API 尚、2023 年 7 月現在、 リモートモデルはプレビュー機能となっています。 ML.GENERATE_TEXT 関数 概要 ML.GENERATE_TEXT 関数 とは、リモートモデル (Vertex AI の text-bison の基盤モデル) と組み合わせることで、BigQuery に保存されているテキストに対して自然言語生成タスクを実行できます。 言語タスクには、以下の例が挙げられます。 分類 感情分析 エンティティの抽出 抽出的な質問への回答 要約 テキストを別のスタイルで書き直す 広告コピーの生成 コンセプトのアイデア出し 尚、2023 年 7 月現在、 ML.GENERATE_TEXT 関数はプレビュー機能となっています。 引数 ML.GENERATE_TEXT 関数の引数には、Vertex AI の基盤モデル (言語) でモデルに送信できるTop-K や Top-P、Temperature 等のパラメータ値と同等の引数を設定できます。 パラメータ値がどのような役割なのかは、以下のブログにも記載しているのでご参照下さい。 参考: パラメータ値 また、 flatten_json_output という引数は、flatten_json_output が TRUE の場合、JSON 形式のレスポンスに含まれる LLM の出力結果と 安全属性の信頼スコアリング の出力結果を個別の列で出力する引数です。 参考: Arguments 出力 出力は、flatten_json_output が TRUE の時と FALSE のときで出力される列名が変わってきます。デフォルトで FALSE となります。 No flatten_json_output 出力される列名 説明 1 FALSE ml_generate_text_result 基盤モデルからの JSON レスポンス。基盤モデルにより生成されたテキストは content の中に含まれ、安全属性の信頼スコアリングは safetyAttributes に含まれる。 2 FALSE ml_generate_text_status 対応する行の API 応答ステータス。操作が成功した場合、この値は空になる。 3 TRUE ml_generate_text_llm_result 基盤モデルから返される生成されたテキスト。 4 TRUE ml_generate_text_rai_result 基盤モデルから返される安全属性の信頼スコアリング。 5 TRUE or FALSE ml_generate_text_status 対応する行の API 応答ステータス。操作が成功した場合、この値は空になる。 クォータと制限 ML.GENERATE_TEXT 関数は、以下のような制限が適用されています。 1 分あたりの Vertex AI API へのリクエスト数 : デフォルト 60 ジョブあたりの行数 : 10,000 同時実行ジョブの数 : 1 特に、1 回のジョブ実行あたり 10,000 行の制限があるので、行数が多い場合は少し工夫が必要になります。 参考: BigQuery - Quotas and limits - Cloud AI service functions 参考: Vertex AI - Quotas and limits - Request quotas 概要 使用するデータ 使用するデータは BigQuery の一般公開データセット上にある IMDB (Internet Movie Database) のデータセットを使用します。 IMDB とは、映画やテレビ番組等のコンテンツに関連する情報を集めたオンラインデータベースで、キャスト、制作スタッフ、作品の概要、評価、ファンや批評家のレビューなどが含まれており、今回はその中から reviews テーブルを使用します。 reviews テーブルには、「(映画の) レビュー」「(正解の) ラベル」「映画 ID」「映画 URL」等が含まれています。 reviews テーブルのプレビュー 今回やること Vertex AI の基盤モデルを BigQuery のリモートモデルとして設定します。そして、そのリモートモデルと BigQuery ML を用いて「(映画の) レビュー」をインプットに、そのレビュー内容が Positive なのか Negative なのかを判断する感情分析タスクを基盤モデルに実行させていきます。 また、「(正解の) ラベル」も含まれるため、最後に 簡易的な精度も算出してみます。 プロンプト設計 プロンプトとは、言語モデルへ送るリクエストのことです。このプロンプト設計が良ければ、基盤モデルからより良い回答を得られます。 今回、映画のレビューをもとに Positive な内容なのか Negative な内容なのかを判定してほしいため、プロンプトには以下の文章を入力してモデルに対しリクエストを送ります。 Classify the sentiment of this review as Positive or Negative? Review: {映画レビュー内容} Sentiment: 後述の 実行 部分で、SQL を用いて {映画のレビュー内容} 部分に実際のテキストを入力していきます。 参考: 感情分析プロンプト 精度結果 先に結果から申し上げますと、英語で書かれた「(映画の) レビュー」に対し、基盤モデルで感情分析を行った結果、合計 10 回試行した平均の正解率は脅威の 94.3% となりました。 以降に、準備の手順や結果をまとめております。 準備 API の有効化 以下のコマンドを実行し、使用する API を有効化します。 gcloud services enable bigqueryconnection.googleapis.com \ aiplatform.googleapis.com データセットの作成 以下のコマンドを実行し、データセットを作成します。 PROJECT_ID={プロジェクト ID を入力} DATASET_ID={データセット ID を入力} bq --location=US mk \ --dataset \ ${PROJECT_ID}:${DATASET_ID} Connection の作成 以下のコマンドを実行し、Connection を作成します。 CONNECTION_ID={任意の Connection ID を入力} bq mk --connection \ --location=US \ --project_id=${PROJECT_ID} \ --connection_type=CLOUD_RESOURCE ${CONNECTION_ID} Connection リソースを作成すると、自動的に一意のサービスアカウントが生成され、Connection にひも付きます。 以下のコマンドを実行し、サービスアカウントが生成されたか確認します。 bq show --connection ${PROJECT_ID}.us.${CONNECTION_ID} 以下のような出力がでるので、後続作業で利用するためサービスアカウント ID をコピーしておきます。 name friendlyName description Last modified type hasCredential properties ------------------------------------- -------------- ------------- ----------------- ---------------- --------------- ---------------------------------------------------------------------------------------------- 1234556789.{Connection ID} 17 Jul 13:43:53 CLOUD_RESOURCE False {"serviceAccountId": "bqcx-{Project Number}-xxxx@gcp-sa-bigquery-condel.iam.gserviceaccount.com"} サービスアカウントに権限付与 以下のコマンドを実行し、先程 Connection 作成時に自動で生成されたサービスアカウントに対し、プロジェクトリソースの Vertex AI ユーザー ロールを付与します。 SERVICE_ACCOUNT_ID={先程コピーしたサービスアカウント ID} gcloud projects add-iam-policy-binding ${PROJECT_ID} \ --member="serviceAccount:${SERVICE_ACCOUNT_ID}" \ --role="roles/aiplatform.user" リモートモデルの作成 以下のコマンドを BigQuery のクエリエディタで実行し、リモートモデルを作成します。尚、変数は置き換えて下さい。 DATASET_ID : 先ほど作成したデータセット ID MODEL_NAME : 任意のモデル名 CONNECTION_ID : 先ほど作成した Connection ID CREATE OR REPLACE MODEL ${DATASET_ID}.${MODEL_NAME} REMOTE WITH CONNECTION `us.${CONNECTION_ID}` OPTIONS (remote_service_type = 'CLOUD_AI_LARGE_LANGUAGE_MODEL_V1'); 実行 感情分析を実行 以下のコマンドを BigQuery のクエリエディタで実行し、5 件の映画レビューについて感情分析を行います。尚、変数は置き換えて下さい。 SELECT ml_generate_text_llm_result, CAST (JSON_EXTRACT_SCALAR(ml_generate_text_rai_result, ' $.blocked ' ) AS BOOL) AS is_safety_filter_blocked, * EXCEPT (ml_generate_text_llm_result, ml_generate_text_rai_result) FROM ML.GENERATE_TEXT( MODEL `${DATASET_ID}.${MODEL_NAME}`, ( SELECT CONCAT ( ' Classify the sentiment of this review as Positive or Negative? \n Review: ' , review, ' \n Sentiment: ' ) AS prompt, * FROM `bigquery- public -data.imdb.reviews` LIMIT 5 ), STRUCT( 0.2 AS temperature, 100 AS max_output_tokens, TRUE AS flatten_json_output )) 出力結果は以下のとおりです。2 ~4 行目のレコードは、モデルのレスポンスが安全しきい値を超えた ( is_safety_filter_blocked が true ) ため、基盤モデル からの出力 ( ml_generate_text_llm_result )が自動的にブロックされています。尚、このような安全フィルターを削除したい場合は、Google のアカウントチームに問い合わせる必要があります。 参考: 安全しきい値 (Safety thresholds) 結果出力画面 評価 前提条件 今回使用している IMDB データセットの reviews テーブルは、映画レビューとともに、その映画レビューが Psitive な内容なのか Negative な内容なのかを表す正解ラベルも付与されています。したがって、基盤モデルからの出力と正解ラベルを比較することで正解率 (Accuracy) を求めていきたいと思います。 元データが 100,000 件あるため、すべてのレコードを対象とすると処理にかなり時間を要します。そこで今回は、label カラムが Positive から 50 件、Negative から 50 件を抽出したサンプルデータで正解率を求めていきます。尚、安全フィルターに該当するデータは基盤モデルからの出力がないため SQL 中で除外いたします。 実行 以下のコマンドを BigQuery のクエリエディタで実行し、 正解率を求めてみます。尚、変数は置き換えて下さい。 WITH positive AS ( SELECT *, ROW_NUMBER() OVER ( ORDER BY RAND()) AS row_num FROM `bigquery- public -data.imdb.reviews` WHERE label = " Positive " ), negative AS ( SELECT *, ROW_NUMBER() OVER ( ORDER BY RAND()) AS row_num FROM `bigquery- public -data.imdb.reviews` WHERE label = " Negative " ), sample_data AS ( SELECT * FROM positive WHERE row_num <= 50 UNION ALL SELECT * FROM negative WHERE row_num <= 50 ) SELECT COUNTIF(ml_generate_text_llm_result = label) AS correct_answer_number, COUNT (label) AS total_number, COUNTIF(ml_generate_text_llm_result = label) / COUNT (label) AS accuracy FROM ML.GENERATE_TEXT( MODEL `${DATASET_ID}.${MODEL_NAME}`, ( SELECT CONCAT ( ' Classify the sentiment of this review as Positive or Negative? \n Review: ' , review, ' \n Sentiment: ' ) AS prompt, * FROM sample_data ), STRUCT( 0.2 AS temperature, 100 AS max_output_tokens, TRUE AS flatten_json_output )) WHERE CAST (JSON_EXTRACT_SCALAR(ml_generate_text_rai_result, ' $.blocked ' ) AS BOOL) = FALSE 出力結果は以下のとおりです。 出力結果画面 10 回試行し平均値を求めてみると以下のとおりです。 10回試行後の結果まとめ 正解率の平均値は、94.3 % となりました。 G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
当記事では、Cloud Workflows と Dataform を用いてデータ分析パイプラインを構築してみたいと思います。 前提知識 Cloud Workflows Dataform ETL と ELT 概要 今回の構成 Cloud Workflows のスコープ Dataform のスコープ 準備 ディレクトリ構造 main.tf gcf_source_code/etl_raw_data main.py requirements.txt gcf_source_code/etl_weather_data main.py requirements.txt source_data 実行 Terraform 実行 Dataform の設定 概要 difinitions/source/raw_data.sqlx difinitions/source/weather_data.sqlx difinitions/transform/mart_data.sqlx 動作検証 検証 1 検証 2 検証 3 クリーンアップ 本番運用時の考慮事項 エラー発生時の通知機能 再試行時の考慮 前提知識 Cloud Workflows Cloud Workflows はGoogle Cloud のワークフロー管理サービスです。フルマネージドかつサーバーレスであるためインフラの管理は必要なく、また非常に安価に利用できるのが特徴です。 詳細については、以下の記事をご参照下さい。 blog.g-gen.co.jp Dataform Dataform は、BigQuery のための SQL ワークフロー管理サービスです。フルマネージドであり、Dataform の利用自体は無料で利用できる点が特徴です。 詳細については、以下の記事をご参照下さい。 blog.g-gen.co.jp ETL と ELT ETL と ELT はどちらもデータを変換処理する際の流れを説明しています。 ETL は、Extract (抽出)、 Transform (変換) 、Load (書き出し) の順序でデータの変換処理を行います。主に、データを利用しやすい形に変換したり、DWH が読み込める形に変換して DWH 等の分析基盤に格納する際に利用します。 ELT は、Extract (抽出)、 Load (書き出し) 、Transform (変換) の順序でデータの変換処理を行います。ETL は変換処理後に DWH 等の分析基盤にデータをロードしますが、ELT の場合、先に DWH 等の分析基盤にデータをロードし、DWH 内でデータの変換処理を行います。 ETL と ELT の違い 概要 今回の構成 BigQuery に分析対象のデータがある場合、 Dataform を用いることで BigQuery 内で複数の SQL の依存関係を管理しつつデータ変換 (ELT) を行うことができます。 しかし、データレイクを Cloud Storage やオンプレミスに構えており、必要に応じ分析対象のデータのみを BigQuery にインポートするケースも多いです。 その際、BigQuery にインポートするデータが、BigQuery のデータの取り込み方式に従っている必要があり、もし対応していない場合はデータ転送時にデータの加工 (ETL) を行う必要があります。 今回は、データレイクと見立てた Cloud Storage のデータを、一部加工を行い BigQuery に格納後、SQL を用いてマートテーブルを作成するデータパイプラインのワークフローを構築します。 今回構築する構成図 Cloud Workflows のスコープ Cloud Workflows では、疎結合になっている Cloud Functions (ETL 処理用) と、Dataform (ELT 処理用) の依存関係を確立しながらワークフローを管理します。 まずはじめに、parallel_step でそれぞれの Cloud Functions を並列で呼び出しております。Cloud Functions では、BigQuery に格納できる最低限のデータ変換処理を行うため、各 csv に対し以下の変換処理を行います。 カラム名を日本語からローマ字に変換 日付のフォーマットを YYYY/MM/DD から YYYY-MM-DD に変換 次に、execute_elt で Dataform の呼び出しとステータス確認を行っております。ポイントは、Dataform の呼び出し ( create_workflow_invocation ) とステータス確認 ( get_workflow_invocation ) は別の API 実行として設定します。理由は、Dataform の呼び出し API のレスポンスにもステータスは返ってきますが、API 実行直後のステータスとなるため、SQL ワークフローの完了を待たずすて RUNNING 等のステータスが返却されるケースがあります。ステータス確認後、ステータスの状態によって条件分岐で処理を分けております。 参考: Dataform ワークフロー呼び出し時のステータス一覧 Cloud Workflows で定義したワークフロー図 Dataform のスコープ Dataform では、BigQuery に格納されたデータの変換処理 (SQL) における依存関係を管理します。今回は検証のため 2 つのテーブルを結合してマートテーブルを作成する簡易的な変換処理を行っております。 Dataform 内の SQL ワークフロー 準備 ディレクトリ構造 開発環境は Cloud Shell を用いて行います。ディレクトリ構造は以下のとおりです。 terraform ディレクトリ配下は、以下のとおりです。 terraform |-- gcf_source_code | |-- etl_raw_data | | |-- main.py | | `-- requirements.txt | ` -- etl_weather_data | |-- main.py | `-- requirements.txt | -- main.tf ` -- source_data |-- raw_data.csv `-- weather_data.csv main.tf main.tf には Terraform のコードを記述しています。 locals { terraform_service_account = ${Terraform 実行に使われるサービスアカウントのメールアドレス} project_name = ${プロジェクト名} project_id = ${プロジェクト ID} folder_id = ${フォルダ ID} billing_account_id = ${請求先アカウント ID} } # terraform & provider の設定 terraform { required_providers { google = { source = "hashicorp/google" version = ">= 4.0.0" } } required_version = ">= 1.3.0" backend "gcs" { bucket = ${tfstate ファイルを格納する Cloud Storage バケット名} impersonate_service_account = ${Terraform 実行に使われるサービスアカウントのメールアドレス} } } # サービスアカウント権限借用の設定 provider "google" { alias = "impersonation" scopes = [ "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", ] } data "google_service_account_access_token" "default" { provider = google.impersonation target_service_account = local.terraform_service_account scopes = ["userinfo-email", "cloud-platform"] lifetime = "1200s" } # Google プロバイダの設定 provider "google" { project = local.project_id region = "asia-northeast1" access_token = data.google_service_account_access_token.default.access_token request_timeout = "60s" } provider "google-beta" { project = local.project_id region = "asia-northeast1" access_token = data.google_service_account_access_token.default.access_token request_timeout = "60s" } ###################################### ### プロジェクトの作成と API の有効化 ### ###################################### # プロジェクトの作成 resource "google_project" "poc" { name = local.project_name project_id = local.project_id folder_id = local.folder_id billing_account = local.billing_account_id } # API の有効化 module "tenant_a_project_services" { source = "terraform-google-modules/project-factory/google//modules/project_services" version = "14.2.1" # version = "~> 13.0" project_id = google_project.poc.project_id enable_apis = true activate_apis = [ "iam.googleapis.com", "cloudbuild.googleapis.com", "run.googleapis.com", "cloudfunctions.googleapis.com", "cloudscheduler.googleapis.com", "artifactregistry.googleapis.com", "workflows.googleapis.com", "bigquery.googleapis.com", "dataform.googleapis.com", ] disable_services_on_destroy = false } ####################################### ### サービスアカウントの作成と権限の付与 ## ####################################### # Dataform サービスアカウントに権限付与 resource "google_project_iam_member" "bq_jobuser" { depends_on = [ module.tenant_a_project_services, google_dataform_repository.dataform_repository ] project = google_project.poc.project_id role = "roles/bigquery.jobUser" member = "serviceAccount:service-${google_project.poc.number}@gcp-sa-dataform.iam.gserviceaccount.com" } resource "google_project_iam_member" "bq_data_editer" { depends_on = [ module.tenant_a_project_services, google_dataform_repository.dataform_repository ] project = google_project.poc.project_id role = "roles/bigquery.dataEditor" member = "serviceAccount:service-${google_project.poc.number}@gcp-sa-dataform.iam.gserviceaccount.com" } # Cloud Functions 用サービスアカウントの作成と権限付与 resource "google_service_account" "sa_gcf" { project = google_project.poc.project_id account_id = "sa-gcf" display_name = "Cloud Functions 用サービスアカウント" } resource "google_project_iam_member" "invoke_gcf" { project = google_project.poc.project_id role = "roles/run.invoker" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_project_iam_member" "bq_jobuser2" { project = google_project.poc.project_id role = "roles/bigquery.jobUser" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_bigquery_dataset_iam_member" "source_dataset_viewer2" { project = google_project.poc.project_id dataset_id = google_bigquery_dataset.source_dataset.dataset_id role = "roles/bigquery.dataEditor" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_bigquery_dataset_iam_member" "mart_dataset_viewer2" { project = google_project.poc.project_id dataset_id = google_bigquery_dataset.mart_dataset.dataset_id role = "roles/bigquery.dataEditor" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_storage_bucket_iam_member" "source_data" { bucket = google_storage_bucket.source_data.name role = "roles/storage.admin" member = "serviceAccount:${google_service_account.sa_gcf.email}" } resource "google_storage_bucket_iam_member" "source_gcf" { bucket = google_storage_bucket.source_gcf.name role = "roles/storage.admin" member = "serviceAccount:${google_service_account.sa_gcf.email}" } # Cloud Workflows 用サービスアカウント作成と権限付与 resource "google_service_account" "sa_wf" { project = google_project.poc.project_id account_id = "sa-cloud-wf" display_name = "Cloud Workflows 用サービスアカウント" } resource "google_project_iam_member" "invoke_gcf2" { project = google_project.poc.project_id role = "roles/run.invoker" member = "serviceAccount:${google_service_account.sa_wf.email}" } resource "google_project_iam_member" "invoke_dataform" { project = google_project.poc.project_id role = "roles/dataform.editor" member = "serviceAccount:${google_service_account.sa_wf.email}" } # Cloud Scheduler 用サービスアカウント作成と権限付与 resource "google_service_account" "sa_scheduler" { project = google_project.poc.project_id account_id = "sa-scheduler" display_name = "Cloud Scheduler 用サービスアカウント" } resource "google_project_iam_member" "workflow_invoker" { project = google_project.poc.project_id role = "roles/workflows.invoker" member = "serviceAccount:${google_service_account.sa_scheduler.email}" } ################################ ### バケットとオブジェクトの作成 ### ################################ # ソースデータ格納用バケットの作成 resource "google_storage_bucket" "source_data" { project = google_project.poc.project_id location = "ASIA-NORTHEAST1" name = "${google_project.poc.project_id}-source-data" force_destroy = true } # ソースデータをバケットに追加 resource "google_storage_bucket_object" "source_raw_data" { name = "raw_data.csv" bucket = google_storage_bucket.source_data.name source = "source_data/raw_data.csv" content_type = "text/csv" } resource "google_storage_bucket_object" "source_weather_data" { name = "weather_data.csv" bucket = google_storage_bucket.source_data.name source = "source_data/weather_data.csv" content_type = "text/csv" } # Cloud Functions のソースコード格納用バケットの作成 resource "google_storage_bucket" "source_gcf" { project = google_project.poc.project_id location = "ASIA-NORTHEAST1" name = "${google_project.poc.project_id}-source-gcf" force_destroy = true } # Cloud Functions で使うソースコードを ZIP 化 data "archive_file" "etl_raw_data" { type = "zip" source_dir = "./gcf_source_code/etl_raw_data" output_path = "./zip_source_code/etl_raw_data.zip" } data "archive_file" "etl_weather_data" { type = "zip" source_dir = "./gcf_source_code/etl_weather_data" output_path = "./zip_source_code/etl_weather_data.zip" } # ZIP 化したソースコードをバケットに追加 resource "google_storage_bucket_object" "etl_raw_data" { name = "etl-raw-data.${data.archive_file.etl_raw_data.output_md5}.zip" bucket = google_storage_bucket.source_gcf.name source = data.archive_file.etl_raw_data.output_path } resource "google_storage_bucket_object" "etl_weather_data" { name = "etl-weather-data.${data.archive_file.etl_weather_data.output_md5}.zip" bucket = google_storage_bucket.source_gcf.name source = data.archive_file.etl_weather_data.output_path } ################################ ### BigQuery データセット作成 ### ################################ # データセットの作成 resource "google_bigquery_dataset" "source_dataset" { project = google_project.poc.project_id dataset_id = "source_dataset" location = "asia-northeast1" delete_contents_on_destroy = true } resource "google_bigquery_dataset" "mart_dataset" { project = google_project.poc.project_id dataset_id = "mart_dataset" location = "asia-northeast1" delete_contents_on_destroy = true } ############################## ### Dataform リポジトリ作成 ### ############################## # Dataform リポジトリ作成 resource "google_dataform_repository" "dataform_repository" { provider = google-beta name = "dataform_repository" } ############################ ### Cloud Functions 作成 ### ############################ # etl_raw_data 関数の作成 resource "google_cloudfunctions2_function" "etl_raw_data" { name = "etl-raw-data" location = "asia-northeast1" build_config { runtime = "python310" entry_point = "excute_etl" # Set the entry point source { storage_source { bucket = google_storage_bucket.source_gcf.name object = google_storage_bucket_object.etl_raw_data.name } } } service_config { max_instance_count = 3 available_memory = "256M" timeout_seconds = 60 service_account_email = google_service_account.sa_gcf.email environment_variables = { BUCKET_NAME = google_storage_bucket.source_data.name FILE_PATH = google_storage_bucket_object.source_raw_data.name DATASET_ID = google_bigquery_dataset.source_dataset.dataset_id TABLE_ID = "raw_data" } } } # etl_weather_data 関数の作成 resource "google_cloudfunctions2_function" "etl_weather_data" { name = "etl-weather-data" location = "asia-northeast1" build_config { runtime = "python310" entry_point = "excute_etl" # Set the entry point source { storage_source { bucket = google_storage_bucket.source_gcf.name object = google_storage_bucket_object.etl_weather_data.name } } } service_config { max_instance_count = 3 available_memory = "256M" timeout_seconds = 60 service_account_email = google_service_account.sa_gcf.email environment_variables = { BUCKET_NAME = google_storage_bucket.source_data.name FILE_PATH = google_storage_bucket_object.source_weather_data.name DATASET_ID = google_bigquery_dataset.source_dataset.dataset_id TABLE_ID = "weather_data" } } } ############################ ### Cloud Scheduler 作成 ### ############################ # Cloud Scheduler の作成 resource "google_cloud_scheduler_job" "cron" { name = "cron" region = "asia-northeast1" schedule = "0 10 * * MON-FRI" # Run the job every weekday at 10:00 AM time_zone = "Asia/Tokyo" http_target { http_method = "POST" uri = "https://workflowexecutions.googleapis.com/v1/projects/${local.project_id}/locations/${google_workflows_workflow.etl_and_elt.region}/workflows/${google_workflows_workflow.etl_and_elt.name}/executions" oauth_token { service_account_email = google_service_account.sa_scheduler.email } body = base64encode(jsonencode({ "argument": jsonencode({ "raw_data_gcf_url" = "${google_cloudfunctions2_function.etl_raw_data.service_config[0].uri}", "weather_data_dcf_url" = "${google_cloudfunctions2_function.etl_weather_data.service_config[0].uri}", "project_id" = "${google_project.poc.project_id}", "repository" = "projects/${google_project.poc.project_id}/locations/asia-northeast1/repositories/${google_dataform_repository.dataform_repository.name}" }) })) headers = { "Content-Type" = "application/json" } } retry_config { retry_count = 3 } } ############################ ### Cloud Workflows 作成 ### ############################ # Cloud Workflows の作成 resource "google_workflows_workflow" "etl_and_elt" { name = "daily-workflows" region = "asia-northeast1" service_account = google_service_account.sa_wf.email source_contents = <<-EOF main: params: [args] steps: - init: assign: - repository: $${args.repository} - raw_data_gcf_url: $${args.raw_data_gcf_url} - weather_data_dcf_url: $${args.weather_data_dcf_url} - parallel_step: parallel: branches: - etl_01: steps: - etl_raw_data: call: http.post args: url: $${raw_data_gcf_url} auth: type: OIDC result: run_name - etl_02: steps: - etl_weather_data: call: http.post args: url: $${weather_data_dcf_url} auth: type: OIDC result: run_name - execute_elt: steps: - create_compilation_result: call: http.post args: url: $${"https://dataform.googleapis.com/v1beta1/" + repository + "/compilationResults"} auth: type: OAuth2 body: gitCommitish: main result: compilationResult - create_workflow_invocation: call: http.post args: url: $${"https://dataform.googleapis.com/v1beta1/" + repository + "/workflowInvocations"} auth: type: OAuth2 body: compilation_result: $${compilationResult.body.name} result: workflowInvocation - complete: return: $${workflowInvocation.body.name} EOF } gcf_source_code/etl_raw_data main.py gcf_source_code/etl_raw_data には、Cloud Storage バケットに格納された raw_data.cev の ETL 処理を行う Cloud Functions のソースコードを格納しています。 import os import functions_framework from io import BytesIO import pandas as pd from google.cloud import storage from google.cloud import bigquery BUCKET_NAME = os.environ.get( "BUCKET_NAME" ) FILE_PATH = os.environ.get( "FILE_PATH" ) DATASET_ID = os.environ.get( "DATASET_ID" ) TABLE_ID = os.environ.get( "TABLE_ID" ) try : # クライアントをインスタンス化 storage_client = storage.Client() bigquery_client = bigquery.Client() except Exception as e: print (f "An error occurred: {e}" ) raise e def extract (): try : # バケットを取得 bucket = storage_client.get_bucket(BUCKET_NAME) # BLOB を構成 blob = bucket.blob(FILE_PATH) # オブジェクトのデータを取得 content = blob.download_as_bytes() except Exception as e: print (f "An error occurred: {e}" ) raise e # データフレームを作成 df = pd.read_csv(BytesIO(content)) return df def transform (df): # カラム名を変更 df = df.rename(columns = { "日付" : "date" , "デバイスID" : "device_id" , "発電量" : "electric_generating_capacity" , "都道府県" : "prefectures" }) # 日付のフォーマットを変更 df[ "date" ] = pd.to_datetime(df[ "date" ]).dt.strftime( "%Y-%m-%d" ) return df def load (df): try : # テーブル情報を取得 table_ref = bigquery_client.dataset(DATASET_ID).table(TABLE_ID) # BigQueryテーブルへデータを挿入 job = bigquery_client.load_table_from_dataframe(df, table_ref) # 結果を確認 job.result() except Exception as e: print (f "An error occurred: {e}" ) raise e print ( "Loaded dataframe to {}" .format(table_ref.path)) return table_ref.path @ functions_framework.http def excute_etl (request): df = extract() df_after_dransform = transform(df) path = load(df_after_dransform) return path requirements.txt functions-framework==3.* bytesbufio==1.0.3 pandas==2.0.3 google-cloud-storage==2.10.0 google-cloud-bigquery==3.11.3 pyarrow==12.0.1 gcf_source_code/etl_weather_data main.py gcf_source_code/etl_weather_data には、Cloud Storage バケットに格納された weather_data.cev の ETL 処理を行う Cloud Functions のソースコードを格納しています。 import os import functions_framework from io import BytesIO import pandas as pd from google.cloud import storage from google.cloud import bigquery BUCKET_NAME = os.environ.get( "BUCKET_NAME" ) FILE_PATH = os.environ.get( "FILE_PATH" ) DATASET_ID = os.environ.get( "DATASET_ID" ) TABLE_ID = os.environ.get( "TABLE_ID" ) try : # クライアントをインスタンス化 storage_client = storage.Client() bigquery_client = bigquery.Client() except Exception as e: print (f "An error occurred: {e}" ) raise e def extract (): try : # バケットを取得 bucket = storage_client.get_bucket(BUCKET_NAME) # BLOB を構成 blob = bucket.blob(FILE_PATH) # オブジェクトのデータを取得 content = blob.download_as_bytes() except Exception as e: print (f "An error occurred: {e}" ) raise e # データフレームを作成 df = pd.read_csv(BytesIO(content)) return df def transform (df): # カラム名を変更 df = df.rename(columns = { "日付" : "date" , "都道府県" : "prefectures" , "気温" : "temperature" , "降水量" : "precipitation" }) # 日付のフォーマットを変更 df[ "date" ] = pd.to_datetime(df[ "date" ]).dt.strftime( "%Y-%m-%d" ) return df def load (df): try : # テーブル情報を取得 table_ref = bigquery_client.dataset(DATASET_ID).table(TABLE_ID) # BigQueryテーブルへデータを挿入 job = bigquery_client.load_table_from_dataframe(df, table_ref) # 結果を確認 job.result() except Exception as e: print (f "An error occurred: {e}" ) raise e print ( "Loaded dataframe to {}" .format(table_ref.path)) return table_ref.path @ functions_framework.http def excute_etl (request): df = extract() df_after_dransform = transform(df) path = load(df_after_dransform) return path requirements.txt functions-framework==3.* bytesbufio==1.0.3 pandas==2.0.3 google-cloud-storage==2.10.0 google-cloud-bigquery==3.11.3 pyarrow==12.0.1 source_data source_data には、Cloud Storage に格納する raw_data.csv と weather_data.csv をそれぞれ配置します。 対象の csv は、以下のスプレッドシートからダウンロードが可能です。 ブログ用ダミーデータ ブログ用ダミーデータ 実行 Terraform 実行 Cloud Shell のターミナルで terraform ディレクトリに移動し、 terraform init で初期化を行い、 terraform plan 問題なければ terraform apply でデプロイを行います。 Dataform の設定 概要 今回、Terraform では Dataform のリポジトリ作成までを行いましたが sqlx ファイルの作成はできていないため、コンソールから以下の手順で sqlx ファイルを作成しリポジトリを完成させていきます。 Dataform > 作成したリポジトリ をクリック > 開発ワークスペースを作成 をクリック 任意のワークスペース名を入力して 作成 をクリック 2 で作成したワークスペースを選択して ワークスペースを初期化 をクリック ワークスペースの初期化を行うと以下のようなファイル群が自動生成されます。 開発ワークスペースの初期化後ファイル群 definitions ディレクトリ内に sqlx ファイルを作成し、SQL ワークフローを定義していきます。definitions ディレクトリ配下を以下の構成に変更してください。 修正後の開発ワークスペースファイル群 各ファイルの中身を以下のように記述します。 difinitions/source/raw_data.sqlx config { type: "declaration", database: ${BigQuery が属するプロジェクト ID}, schema: "source_dataset", name: "raw_data", } 参考: Dataform 徹底解説 データソースの宣言 difinitions/source/weather_data.sqlx config { type: "declaration", database: ${BigQuery が属するプロジェクト ID}, schema: "source_dataset", name: "weather_data", } difinitions/transform/mart_data.sqlx config { type: "table", schema: "mart_dataset", } SELECT A.date, A.device_id, A.electric_generating_capacity, A.prefectures, B.temperature, B.Precipitation FROM ${ref("raw_data")} as A LEFT JOIN ${ref("weather_data")} as B ON A.date = B.date AND A.prefectures = B.prefectures ファイル修正後は、 COMMIT とフォルトブランチへの PUSH を行ってください。 動作検証 検証 1 検証 1 は、そのまま実行してみます。尚、今回は、Cloud Scheduler を手動で強制実行していきたいと思います。 コンソールにて、Cloud Scheduler > 作成したジョブの 操作 から 強制実行 をクリックします。 Cloud Scheduler のコンソール画面 すると、Cloud Workflows がトリガーされ実行されます。以下が Cloud Workflows の実行詳細画面です。 Cloud Workflows のワークフローが成功したことが確認できました。 [検証1] Cloud Workflows の実行詳細画面 また、以下が Dataform の実行詳細画面です。Dataform も無事成功したことが確認できました。 Dataform の実行詳細画面 一連のワークフローが完了したことが確認できたため、最後に BigQuery 上でマートテーブルが作成できているか確認します。 BigQuery のコンソール画面 無事マートテーブルも作成できていました。 [検証1] Cloud Workflows のワークフロー図 検証 2 検証 2 では、Cloud Storage バケット上の raw_data.csv ファイルを削除してから、Cloud Scheduler を強制実行してみます。 以下が Cloud Workflows の実行詳細画面です。Cloud Functions を実行している parallel_step でエラーが発生したので、想定通りの挙動を確認できました。 [検証2] Cloud Workflows 実行詳細画面 [検証2] Cloud Workflows のワークフロー図 検証 3 検証 3 では、Cloud Storage バケット上のファイルを検証 1 の状態に戻し、Dataform 上の definitions/source/raw_data.sqlx ファイルを以下のように存在しない schema に書き換えてみます。 config { type: "declaration", database: "matayuuu-etl-elt", schema: "hoge_dataset", name: "raw_data", } ファイル修正後は、COMMIT とフォルトブランチへの PUSH を行い、Cloud Scheduler を強制実行してみます。 以下が Cloud Workflows の実行詳細画面です。Dataform のステータス確認を行っている check_if_complete でエラーが発生したので、想定通りの挙動を確認できました。 [検証3] Cloud Workflows の実行詳細画面 [検証3] Cloud Workflows のワークフロー図 クリーンアップ Dataform の開発ワークスペースを削除し、作成した Terraform を destroy コマンドで削除します。 BigQuery > Dataform > リポジトリを選択し、 手動で作成した開発ワークスペース を削除 Cloud Shell にて terraform destroy を実行 本番運用時の考慮事項 エラー発生時の通知機能 今回は検証のため Cloud Workflows がエラーになってもメールや Slack 通知等で管理者へ通知する仕組みは作っておりませんが、本番運用時にはエラー発生時に即座に対応できるようにしておくとよいでしょう。 例えば、Cloud Workflows のログは Cloud Logging と連携しているため、 ログベースのアラート を構成することで容易にエラー検知ができます。 再試行時の考慮 何らかの理由で Cloud Workflows のワークフローや Dataform の SQL ワークフローが途中で失敗した際に、それぞれのワークフローを再実行しても同じ結果を得るように設計しておくことが重要となります。 そこで使われるのが 冪等性 の担保です。何度繰り返しても、いつ実行しても、特定の入力セットに対して同じ動作をすることを冪等性が保たれている状態です。 その他にも、可能であればチェックポイントを設定することで、ワークフローが再開された際に、最初からジョブを再開するのではなく、中断したところから再開できるようにする方法も検討してみると良いでしょう。 参考: Jobs retries and checkpoints best practices G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
G-gen の佐々木です。当記事では Google Kubernetes Engine(以下、GKE)で 予備の容量プロビジョニング(spare capacity provisioning) を使用することで、ワークロードを素早くスケールアウトする方法を解説します。 GKE とは ノードの自動プロビジョニングを使用したスケールアウトの問題点 予備の容量プロビジョニングについて 予備の容量プロビジョニングの概要 一貫した容量のプロビジョニング 単一イベント容量のプロビジョニング 2 つの方法の比較 予備の容量プロビジョニングを使用する 使用するマニフェストファイル priorityclasses.yaml test-deployment.yaml capacity-res-deployment.yaml(一貫した容量のプロビジョニングで使用) capacity-res-job.yaml(単一イベント容量のプロビジョニングで使用) GKE クラスタの作成 予備の容量プロビジョニングを使用しない場合 一貫した容量のプロビジョニングを使用する PriorityClass オブジェクトを作成する プレースホルダ Pod をデプロイする ワークロード用 Pod のデプロイ 単一イベント容量のプロビジョニングを使用する ノードの初期状態を確認する PriorityClass オブジェクトを作成する プレースホルダ Pod をデプロイする ワークロード用 Pod のデプロイ Google Kubernetes Engine(GKE) GKE とは GKE はコンテナオーケストレーションツールである Kubernetes を、Google マネージドのクラスタで使用することができるサービスです。 GKE ではノードの管理をユーザーが行うことで柔軟な要件に対応できる Standard モード のクラスタと、ノードの管理を Google に任せ、ワークロードの管理のみに集中することができる Autopilot モード のクラスタを選択することができます。 GKE の詳細については以下の記事で解説しています。 blog.g-gen.co.jp ノードの自動プロビジョニングを使用したスケールアウトの問題点 ノードの自動プロビジョニング が有効化されている GKE クラスタでは、Pod がスケールアウトするとき、既存のノードに新しい Pod を起動するための容量がない場合に新しいノードが自動で作成されます。 しかし、新しいノードの起動には約 80 秒 ~ 120 秒かかるため、急激なトラフィック増加などに素早く対応できない場合があります。 ノードの起動待ちでワークロードのスケールアウトに時間がかかる 特に Autopilot モードのクラスタでは、ノードのコンピューティングリソース量をユーザーが指定することができないため、スケールアウト時のリソース使用量を考慮してノードのマシンタイプを設定したり、事前にノード数を増やしておいたりして対策することが難しくなっています。 当記事では、事前にノード数を増やしておき Pod をすぐに起動できるようにする方法として、 予備の容量プロビジョニング を使用します。当機能は Autopilot クラスタと Standard クラスタの両方で利用可能です。 予備の容量プロビジョニングについて 予備の容量プロビジョニングの概要 予備の容量プロビジョニング では、Kubernetes の PriorityClass オブジェクトを使用し、一定数の 優先度の低い Pod をノードで実行します。こうすることで予めノードを増やしておくことができ、ワークロードの Pod がスケールアウトする際に、優先度の低い Pod を終了してワークロードの Pod に置き換えることができます。 ここで使用する優先度の低い Pod は プレースホルダ Pod 、または バルーン Pod と呼ばれます。当記事では公式ドキュメントに従いプレースホルダ Pod と記載します。 プレースホルダ Pod を使用してワークロードのスケールアウトを高速化する 予備の容量プロビジョニングでは、プレースホルダ Pod の実行の仕方によって、 一貫した容量のプロビジョニング と 単一のイベント容量のプロビジョニング の 2種類の方法を使うことができます。 参考: Pod の迅速なスケーリングのために追加のコンピューティング容量をプロビジョニングする 一貫した容量のプロビジョニング 一貫した容量のプロビジョニング では、Deployment を使用することで、クラスタ内で常に実行されるプレースホルダ Pod を配置します。 プレースホルダ Pod の実行に必要な容量だけノードがプロビジョニングされ、ワークロードのスケールアウトが必要になった際はプレースホルダ Pod に置き換える形で Pod を追加することができます。 プレースホルダ Pod はワークロードの Pod に置き換えられますが、Deployment を使用してデプロイされているために、一度ワークロードの Pod に置き換えられたあと、プレースホルダ Pod の数を維持しようとします。そのため、GKE クラスタは新たなノードを追加してプレースホルダ Pod を再作成します。 したがって、クラスタにはワークロードを実行するために必要な容量よりも多くの容量が常に確保される点には注意が必要です。 一貫した容量のプロビジョニングを使用した場合のスケールアウト動作のイメージ 単一イベント容量のプロビジョニング 単一イベント容量のプロビジョニング では、Job を使用することで特定の期間だけプレースホルダ Pod を起動し、その実行に必要な容量だけノードをプロビジョニングします。 ワークロードのスケールアウトが必要になると、プレースホルダ Pod がワークロードの Pod と置き換わる点は一貫した容量のプロビジョニングと同様ですが、Job で実行されているため、置き換わったあとに Pod が再作成されることはありません。 単一イベント容量のプロビジョニングを使用した場合のスケールアウト動作のイメージ 2 つの方法の比較 プレースホルダ Pod の作成に Job を使用する場合(単一イベント容量のプロビジョニング)は Deployment を使用する場合(一貫した容量のプロビジョニング)とは異なり、終了したプレースホルダ Pod を再作成するような動作はしないため、プレースホルダ Pod の再作成するためにノードが追加されることはありません。したがって、プレースホルダ Pod 用に確保される容量は Job を使用したほうが抑えられます。 GKE の Standard モードでは起動しているノードあたりの時間料金、Autopilot モードでは Pod がリクエストしているリソース量あたりの時間料金が発生するため、必要なときだけプレースホルダ Pod を実行するほうにコストメリットがあります。 ただし、Job を使用する場合はワークロードがスケールアウトするタイミングをある程度は把握しておき、それに合わせて Job を作成しておく必要があります。また、一度置き換えられたプレースホルダ Pod は再作成されないため、一日に何度もスケーリングをする必要があるワークロードの場合には向きません。Deployment を使用していればプレースホルダ Pod は常に存在するため、ワークロードがスケールアウトする余裕を持ち続けることができます。 予備の容量プロビジョニングを使用する ここからは、予備の容量プロビジョニングを使用することで、Pod の起動時間が改善されるかどうかを検証します。 使用するマニフェストファイル 当記事で使用するマニフェストファイルは、 公式ドキュメント に記載されているものを参考にしています。 priorityclasses.yaml Pod に紐付けることができる PriorityClass オブジェクトを 2つ作成するマニフェストファイルです。 優先度は value フィールドに設定し、数値が大きいほど Pod の実行優先度が高くなります。 # priorityclasses.yaml apiVersion : scheduling.k8s.io/v1 kind : PriorityClass metadata : name : low-priority value : -10 # この PriorityClass を使用する Pod の優先度(値が小さいほど優先度が低い) preemptionPolicy : Never # この PriorityClass を使用する Pod は、これより優先度の低い Pod を削除しない(=Never) globalDefault : false description : "Low priority workloads" --- apiVersion : scheduling.k8s.io/v1 kind : PriorityClass metadata : name : default-priority value : 0 # この PriorityClass を使用する Pod の優先度 preemptionPolicy : PreemptLowerPriority # この PriorityClass を使用する Pod は、これより優先度の低い Pod を削除する(=PreemptLowerPriority) globalDefault : true # Pod に PriorityClass が明示的に設定されていない場合、これをデフォルトの PriorityClass とする description : "The global default priority." 1つ目の PriorityClass は低優先度のプレースホルダ Pod に紐付けるもので、優先度の値が -10 に設定されています。 2つ目の PriorityClass は globalDefault フィールドの値が true に設定されており、GKE クラスタにデプロイされる Pod にデフォルトで紐付けられます。 preemptionPolicy フィールドの値が PreemptLowerPriority に設定されているため、この PriorityClass が紐付いた Pod が起動する際にノードの容量が足りていないと、優先度がより低い Pod を削除してから起動するように動作します。 test-deployment.yaml このマニフェストファイルでは、ワークロードの Pod を想定したサンプルの Pod を 5 つ実行する Deployment を作成します。 PriorityClass を指定していないため、 priorityclasses.yaml により作成された PriorityClass がある場合、これらの Pod の優先度は 0 となります。 # test-deployment.yaml apiVersion : apps/v1 kind : Deployment metadata : name : helloweb labels : app : hello spec : replicas : 5 selector : matchLabels : app : hello tier : web template : metadata : labels : app : hello tier : web spec : containers : - name : hello-app image : us-docker.pkg.dev/google-samples/containers/gke/hello-app:1.0 ports : - containerPort : 8080 resources : requests : cpu : 400m memory : 400Mi capacity-res-deployment.yaml(一貫した容量のプロビジョニングで使用) プレースホルダ Pod を作成するためのマニフェストファイルでは、 priorityClassName に低優先度の PriorityClass を指定し、10 個の Pod が優先度 -10 で作成されるようにします。 これらの Pod はノードの容量を予め確保しておき、ワークロードのスケールアウトが必要になったとき、ワークロードの Pod と置き換わります。 # capacity-res-deployment.yaml apiVersion : apps/v1 kind : Deployment metadata : name : capacity-res-deploy spec : replicas : 10 selector : matchLabels : app : reservation template : metadata : labels : app : reservation spec : priorityClassName : low-priority # 低優先度の PriorityClass を指定 terminationGracePeriodSeconds : 0 containers : - name : ubuntu image : ubuntu command : [ "sleep" ] args : [ "infinity" ] resources : requests : cpu : 500m memory : 500Mi capacity-res-job.yaml(単一イベント容量のプロビジョニングで使用) 単一イベント容量のプロビジョニングでは Job を使用してプレースホルダ Pod を実行します。 このマニフェストファイルでは低優先度の Pod を Job として実行し、ワークロード用の Pod による置き換えが起こるか sleep コマンドの処理が終わる(10時間が経過する)と Pod が削除されます。 # capacity-res-job.yaml apiVersion : batch/v1 kind : Job metadata : name : capacity-res-job spec : parallelism : 10 backoffLimit : 0 template : spec : priorityClassName : low-priority terminationGracePeriodSeconds : 0 containers : - name : ubuntu-container image : ubuntu command : [ "sleep" ] args : [ "36000" ] resources : requests : cpu : 500m restartPolicy : Never GKE クラスタの作成 以下のコマンドを使用して、検証用のクラスタを作成します。 当記事では Autopilot モードのクラスタを使用していきます。 # Autopilot モードの GKE クラスタを作成する $ gcloud container clusters create-auto {クラスタ名} \ --region={リージョン} \ --project={プロジェクトID} 参考: Autopilot クラスタの作成 予備の容量プロビジョニングを使用しない場合 まず、予備の容量プロビジョニングを使用しない場合のワークロード用 Pod の起動時間を計測してみます。 作成した GKE クラスタのノードの初期状態を確認します。 # ノードの状態を確認 $ kubectl get nodes # 出力例 $ kubectl get nodes NAME STATUS ROLES AGE VERSION gk3-cluster-sasashun-gke-default-pool-69f9cf5d-m972 Ready <none> 74m v1.26.5-gke.1200 gk3-cluster-sasashun-gke-default-pool-b9c30e3b-z2jn Ready <none> 74m v1.26.5-gke.1200 次に、Pod の起動時間を確認するために、 kubectl get pods コマンドで --watch オプションを使用して Pod のステータスをモニタリングします。 # Pod のステータスをモニタリングする $ kubectl get pods -w 別のターミナルから test-deployment.yaml をクラスタに適用し、ワークロード用の Pod をデプロイします。 # Pod のデプロイ(別のターミナルで実施) $ kubectl apply -f test-deployment.yaml 最初のターミナルで Pod のステータスをモニタリングしているため、各 Pod の STATUS 列が Running になるまで待機します。 今回の検証時の出力を以下に記載します。Pod がすべて実行されるまで、2分 30秒ほど要したことがわかります。 # 出力例 $ kubectl get pods -w NAME READY STATUS RESTARTS AGE helloweb-75f7cfd4f7-mnn8f 0/1 Pending 0 1s helloweb-75f7cfd4f7-mnn8f 0/1 Pending 0 1s helloweb-75f7cfd4f7-gz58p 0/1 Pending 0 0s helloweb-75f7cfd4f7-gz58p 0/1 Pending 0 0s helloweb-75f7cfd4f7-hhf62 0/1 Pending 0 0s helloweb-75f7cfd4f7-hhf62 0/1 Pending 0 0s helloweb-75f7cfd4f7-sng6f 0/1 Pending 0 0s helloweb-75f7cfd4f7-6j9sx 0/1 Pending 0 0s helloweb-75f7cfd4f7-sng6f 0/1 Pending 0 0s helloweb-75f7cfd4f7-6j9sx 0/1 Pending 0 0s helloweb-75f7cfd4f7-mnn8f 0/1 Pending 0 77s helloweb-75f7cfd4f7-gz58p 0/1 Pending 0 76s helloweb-75f7cfd4f7-hhf62 0/1 Pending 0 76s helloweb-75f7cfd4f7-sng6f 0/1 Pending 0 76s helloweb-75f7cfd4f7-6j9sx 0/1 Pending 0 76s helloweb-75f7cfd4f7-mnn8f 0/1 ContainerCreating 0 77s helloweb-75f7cfd4f7-gz58p 0/1 ContainerCreating 0 76s helloweb-75f7cfd4f7-hhf62 0/1 ContainerCreating 0 76s helloweb-75f7cfd4f7-sng6f 0/1 Pending 0 82s helloweb-75f7cfd4f7-6j9sx 0/1 Pending 0 82s helloweb-75f7cfd4f7-sng6f 0/1 ContainerCreating 0 82s helloweb-75f7cfd4f7-6j9sx 0/1 ContainerCreating 0 82s helloweb-75f7cfd4f7-mnn8f 1/1 Running 0 2m18s helloweb-75f7cfd4f7-gz58p 1/1 Running 0 2m20s helloweb-75f7cfd4f7-hhf62 1/1 Running 0 2m23s helloweb-75f7cfd4f7-sng6f 1/1 Running 0 2m25s helloweb-75f7cfd4f7-6j9sx 1/1 Running 0 2m30s 次に、ノードのスケールアウトが行われたかどうかを確認します。 ワークロードの Pod を実行するために 2つのノードが追加されていることがわかります。ノードの追加を待ってから Pod が起動されるため、ノードの追加待ち時間だけ Pod の起動に時間がかかってしまっています。 # ノードの数を確認する $ kubectl get nodes # 出力例 $ kubectl get nodes NAME STATUS ROLES AGE VERSION gk3-cluster-sasashun-gke-default-pool-69f9cf5d-m972 Ready <none> 79m v1.26.5-gke.1200 gk3-cluster-sasashun-gke-default-pool-b9c30e3b-z2jn Ready <none> 79m v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-5d602c8d-zwcp Ready <none> 2m29s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-ed49f4e8-cgbn Ready <none> 2m25s v1.26.5-gke.1200 Pod を削除し、ノードの数が元に戻るまで待機します。 # Pod を削除する $ kubectl delete -f test-deployment.yaml # ノード数が元に戻ったことを確認 $ kubectl get nodes # 出力例 $ kubectl get nodes $ kubectl get nodes NAME STATUS ROLES AGE VERSION gk3-cluster-sasashun-gke-default-pool-69f9cf5d-m972 Ready <none> 85m v1.26.5-gke.1200 gk3-cluster-sasashun-gke-default-pool-b9c30e3b-z2jn Ready <none> 85m v1.26.5-gke.1200 一貫した容量のプロビジョニングを使用する まず、一貫した容量のプロビジョニングを使用してみます。 この方法では、Deployment によってクラスタ上でプレースホルダ Pod が常に実行されるようにします。 PriorityClass オブジェクトを作成する Pod に優先度を設定するために PriorityClass オブジェクトを作成します。 クラスタに priorityclasses.yaml を適用します。 # PriorityClass のマニフェストを適用する $ kubectl apply -f priorityclasses.yaml プレースホルダ Pod をデプロイする capacity-res-deployment.yaml をクラスタに適用し、プレースホルダ Pod を作成します。 プレースホルダ Pod が全て作成されるまで待機します。 # プレースホルダ Pod のマニフェストを適用する $ kubectl apply -f capacity-res-deployment.yaml # プレースホルダ Pod のステータスを確認する $ kubectl get pods # 出力例 $ kubectl get pods NAME READY STATUS RESTARTS AGE capacity-res-deploy-74b9b79578-4cvms 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-85tgp 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-89qfz 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-8v2dh 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-d4kth 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-dvxrd 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-gvqcp 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-kdxzl 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-kxr4w 1/1 Running 0 2m43s capacity-res-deploy-74b9b79578-rzjvk 1/1 Running 0 2m43s ノードの状態を確認すると、プレースホルダ Pod を作成するためにノードが追加されていることがわかります。 # ノードのスケールアウトを確認する $ kubectl get nodes # 出力例 $ kubectl get nodes NAME STATUS ROLES AGE VERSION gk3-cluster-sasashun-gke-default-pool-69f9cf5d-m972 Ready <none> 90m v1.26.5-gke.1200 gk3-cluster-sasashun-gke-default-pool-b9c30e3b-z2jn Ready <none> 90m v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-144d029c-fjgv Ready <none> 3m2s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-144d029c-q4xg Ready <none> 3m1s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-5d602c8d-p78l Ready <none> 2m59s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-ed49f4e8-67dq Ready <none> 3m1s v1.26.5-gke.1200 ワークロード用 Pod のデプロイ ワークロード用の Pod をデプロイし、Pod の起動時間とプレースホルダ Pod の動作を確認します。 まず、Pod のステータスをモニタリングするために以下のコマンドを実行します。 # Pod のステータスをモニタリングする $ kubectl get pods -w 続いて、別のターミナルで test-deployment.yaml をクラスタに適用し、ワークロード用の Pod をデプロイします。 # ワークロード用 Pod のデプロイ(別のターミナルで実施) $ kubectl apply -f test-deployment.yaml ワークロード用 Pod のマニフェストファイルを適用したあと Pod のステータスをモニタリングしているターミナルを確認すると、優先度が低く設定されているプレースホルダ Pod が削除され、代わりにワークロード用 Pod が作成されていることがわかります。 プレースホルダ Pod が事前にノード容量を確保しているため、ワークロード用 Pod は素早く作成されます。今回の検証では 15秒以内に全てのワークロード用 Pod が起動していることがわかります。 # 出力例 $ kubectl get pods -w NAME READY STATUS RESTARTS AGE capacity-res-deploy-74b9b79578-4cvms 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-85tgp 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-89qfz 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-8v2dh 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-d4kth 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-dvxrd 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-gvqcp 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-kdxzl 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-kxr4w 1/1 Running 0 6m19s capacity-res-deploy-74b9b79578-rzjvk 1/1 Running 0 6m19s helloweb-75f7cfd4f7-plv6r 0/1 Pending 0 0s helloweb-75f7cfd4f7-plv6r 0/1 Pending 0 0s helloweb-75f7cfd4f7-plv6r 0/1 ContainerCreating 0 0s helloweb-75f7cfd4f7-d8tdv 0/1 Pending 0 0s helloweb-75f7cfd4f7-lnbbh 0/1 Pending 0 0s helloweb-75f7cfd4f7-n67w5 0/1 Pending 0 1s helloweb-75f7cfd4f7-nwknn 0/1 Pending 0 1s capacity-res-deploy-74b9b79578-kdxzl 1/1 Running 0 6m51s capacity-res-deploy-74b9b79578-kdxzl 1/1 Terminating 0 6m51s capacity-res-deploy-74b9b79578-kdxzl 1/1 Terminating 0 6m51s helloweb-75f7cfd4f7-d8tdv 0/1 Pending 0 1s capacity-res-deploy-74b9b79578-fdmfl 0/1 Pending 0 0s capacity-res-deploy-74b9b79578-d4kth 1/1 Running 0 6m51s capacity-res-deploy-74b9b79578-d4kth 1/1 Terminating 0 6m51s capacity-res-deploy-74b9b79578-d4kth 1/1 Terminating 0 6m51s helloweb-75f7cfd4f7-lnbbh 0/1 Pending 0 1s capacity-res-deploy-74b9b79578-gvqcp 1/1 Running 0 6m51s capacity-res-deploy-74b9b79578-gvqcp 1/1 Terminating 0 6m51s capacity-res-deploy-74b9b79578-gvqcp 1/1 Terminating 0 6m51s capacity-res-deploy-74b9b79578-p48dn 0/1 Pending 0 0s helloweb-75f7cfd4f7-n67w5 0/1 Pending 0 1s capacity-res-deploy-74b9b79578-dvxrd 1/1 Running 0 6m51s capacity-res-deploy-74b9b79578-dvxrd 1/1 Terminating 0 6m51s capacity-res-deploy-74b9b79578-dvxrd 1/1 Terminating 0 6m51s capacity-res-deploy-74b9b79578-bq9bm 0/1 Pending 0 0s helloweb-75f7cfd4f7-nwknn 0/1 Pending 0 1s capacity-res-deploy-74b9b79578-fdmfl 0/1 Pending 0 0s capacity-res-deploy-74b9b79578-p48dn 0/1 Pending 0 0s capacity-res-deploy-74b9b79578-bq9bm 0/1 Pending 0 0s capacity-res-deploy-74b9b79578-55mx9 0/1 Pending 0 0s capacity-res-deploy-74b9b79578-55mx9 0/1 Pending 0 0s helloweb-75f7cfd4f7-lnbbh 0/1 Pending 0 3s helloweb-75f7cfd4f7-nwknn 0/1 Pending 0 3s helloweb-75f7cfd4f7-n67w5 0/1 Pending 0 3s helloweb-75f7cfd4f7-d8tdv 0/1 Pending 0 3s helloweb-75f7cfd4f7-lnbbh 0/1 ContainerCreating 0 3s helloweb-75f7cfd4f7-d8tdv 0/1 ContainerCreating 0 3s helloweb-75f7cfd4f7-nwknn 0/1 ContainerCreating 0 3s helloweb-75f7cfd4f7-n67w5 0/1 ContainerCreating 0 3s helloweb-75f7cfd4f7-plv6r 1/1 Running 0 11s helloweb-75f7cfd4f7-d8tdv 1/1 Running 0 12s helloweb-75f7cfd4f7-lnbbh 1/1 Running 0 13s helloweb-75f7cfd4f7-nwknn 1/1 Running 0 13s helloweb-75f7cfd4f7-n67w5 1/1 Running 0 14s さらにしばらく待機すると、Deployment に設定されたプレースホルダ Pod の数を維持するために新たなプレースホルダ Pod が作成されます。 # 出力例 $ kubectl get pods NAME READY STATUS RESTARTS AGE capacity-res-deploy-74b9b79578-4cvms 1/1 Running 0 8m53s capacity-res-deploy-74b9b79578-55mx9 1/1 Running 0 2m2s capacity-res-deploy-74b9b79578-85tgp 1/1 Running 0 8m53s capacity-res-deploy-74b9b79578-89qfz 1/1 Running 0 8m53s capacity-res-deploy-74b9b79578-8v2dh 1/1 Running 0 8m53s capacity-res-deploy-74b9b79578-bq9bm 1/1 Running 0 2m2s capacity-res-deploy-74b9b79578-fdmfl 1/1 Running 0 2m2s capacity-res-deploy-74b9b79578-kxr4w 1/1 Running 0 8m53s capacity-res-deploy-74b9b79578-p48dn 1/1 Running 0 2m2s capacity-res-deploy-74b9b79578-rzjvk 1/1 Running 0 8m53s helloweb-75f7cfd4f7-d8tdv 1/1 Running 0 2m3s helloweb-75f7cfd4f7-lnbbh 1/1 Running 0 2m3s helloweb-75f7cfd4f7-n67w5 1/1 Running 0 2m3s helloweb-75f7cfd4f7-nwknn 1/1 Running 0 2m3s helloweb-75f7cfd4f7-plv6r 1/1 Running 0 2m3s ノードの状態を確認すると、置き換えられたぶんのプレースホルダ Pod を実行するために、ノードが追加されていることがわかります。 このように、一貫した容量のプロビジョニングを使用する場合、プレースホルダ Pod 用の容量が常に確保されることになります。 # 出力例 $ kubectl get nodes NAME STATUS ROLES AGE VERSION gk3-cluster-sasashun-gke-default-pool-69f9cf5d-m972 Ready <none> 95m v1.26.5-gke.1200 gk3-cluster-sasashun-gke-default-pool-b9c30e3b-z2jn Ready <none> 95m v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-144d029c-fjgv Ready <none> 8m16s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-144d029c-q4xg Ready <none> 8m15s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-5d602c8d-p78l Ready <none> 8m13s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-ed49f4e8-67dq Ready <none> 8m15s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-2-af991da7-cq9c Ready <none> 86s v1.26.5-gke.1200 単一イベント容量のプロビジョニングを使用する 最後に、単一イベント容量のプロビジョニングを使用して ワークロード用 Pod をデプロイします。 当記事ではクラスタの状態を初期化するために一度クラスタを削除し、再作成しています。 ノードの初期状態を確認する クラスタを再作成したため、まずはノードの初期状態を確認します。 # ノードの状態を確認 $ kubectl get nodes # 出力例 $ kubectl get nodes NAME STATUS ROLES AGE VERSION gk3-cluster-sasashun-gke-default-pool-76ea9d98-xj4v Ready <none> 99s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-default-pool-d830f363-f5bt Ready <none> 98s v1.26.5-gke.1200 PriorityClass オブジェクトを作成する 改めて priorityclasses.yaml を適用し、PriorityClass を作成します。 # PriorityClass のマニフェストを適用する $ kubectl apply -f priorityclasses.yaml プレースホルダ Pod をデプロイする capacity-res-job.yaml をクラスタに適用し、プレースホルダ Pod を実行する Job を作成します。 プレースホルダ Pod が全て作成されるまで待機します。 # プレースホルダ Job のマニフェストを適用する $ kubectl apply -f capacity-res-job.yaml # ジョブのステータスを確認する $ kubectl get jobs # 出力例 $ kubectl get jobs NAME COMPLETIONS DURATION AGE capacity-res-job 0/1 of 10 114s 114s # Job で起動されたプレースホルダ Pod を確認する $ kubectl get pods # 出力例 $ kubectl get pods NAME READY STATUS RESTARTS AGE capacity-res-job-fc8v2 1/1 Running 0 3m4s capacity-res-job-g56jm 1/1 Running 0 3m4s capacity-res-job-ks6j7 1/1 Running 0 3m3s capacity-res-job-lzpqk 1/1 Running 0 3m4s capacity-res-job-n9w6p 1/1 Running 0 3m4s capacity-res-job-nsmks 1/1 Running 0 3m3s capacity-res-job-ph5cp 1/1 Running 0 3m3s capacity-res-job-rh8cq 1/1 Running 0 3m3s capacity-res-job-t7h4t 1/1 Running 0 3m3s capacity-res-job-zxgg4 1/1 Running 0 3m3s ノードの状態を確認すると、プレースホルダ Pod を作成するためにノードが追加されていることがわかります。 # ノードのスケールアウトを確認する $ kubectl get nodes # 出力例 $ kubectl get nodes NAME STATUS ROLES AGE VERSION gk3-cluster-sasashun-gke-default-pool-76ea9d98-xj4v Ready <none> 6m7s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-default-pool-d830f363-f5bt Ready <none> 6m6s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-1dc5c9f6-6g97 Ready <none> 2m17s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-1dc5c9f6-fl7d Ready <none> 2m16s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-4d328f57-bm4l Ready <none> 2m19s v1.26.5-gke.1200 gk3-cluster-sasashun-gke-publi-pool-1-b29f6e44-xkhk Ready <none> 2m19s v1.26.5-gke.1200 ワークロード用 Pod のデプロイ ワークロード用の Pod をデプロイし、Pod の起動時間とプレースホルダ Pod の動作を確認します。 まず、Pod のステータスをモニタリングするために以下のコマンドを実行します。 # Pod のステータスをモニタリングする $ kubectl get pods -w 続いて、別のターミナルで test-deployment.yaml をクラスタに適用し、ワークロード用の Pod をデプロイします。 # ワークロード用 Pod のデプロイ(別のターミナルで実施) $ kubectl apply -f test-deployment.yaml Job によってプレースホルダ Pod がノード容量を確保しているため、ワークロード用 Pod は素早く作成されます。今回も 15秒以内に全てのワークロード用 Pod が起動していることがわかります。 # 出力例 $ kubectl get pods -w NAME READY STATUS RESTARTS AGE capacity-res-job-fc8v2 1/1 Running 0 3m47s capacity-res-job-g56jm 1/1 Running 0 3m47s capacity-res-job-ks6j7 1/1 Running 0 3m46s capacity-res-job-lzpqk 1/1 Running 0 3m47s capacity-res-job-n9w6p 1/1 Running 0 3m47s capacity-res-job-nsmks 1/1 Running 0 3m46s capacity-res-job-ph5cp 1/1 Running 0 3m46s capacity-res-job-rh8cq 1/1 Running 0 3m46s capacity-res-job-t7h4t 1/1 Running 0 3m46s capacity-res-job-zxgg4 1/1 Running 0 3m46s helloweb-75f7cfd4f7-rvh8w 0/1 Pending 0 0s helloweb-75f7cfd4f7-rvh8w 0/1 Pending 0 0s helloweb-75f7cfd4f7-rvh8w 0/1 ContainerCreating 0 0s helloweb-75f7cfd4f7-6wq75 0/1 Pending 0 0s helloweb-75f7cfd4f7-2wblr 0/1 Pending 0 0s helloweb-75f7cfd4f7-5wjvd 0/1 Pending 0 1s helloweb-75f7cfd4f7-llkfh 0/1 Pending 0 1s capacity-res-job-zxgg4 1/1 Running 0 4m7s capacity-res-job-zxgg4 1/1 Terminating 0 4m7s helloweb-75f7cfd4f7-6wq75 0/1 Pending 0 1s capacity-res-job-ph5cp 1/1 Running 0 4m7s capacity-res-job-zxgg4 1/1 Terminating 0 4m7s capacity-res-job-ph5cp 1/1 Terminating 0 4m7s helloweb-75f7cfd4f7-2wblr 0/1 Pending 0 1s capacity-res-job-ph5cp 1/1 Terminating 0 4m7s capacity-res-job-rh8cq 1/1 Running 0 4m7s capacity-res-job-rh8cq 1/1 Terminating 0 4m7s helloweb-75f7cfd4f7-5wjvd 0/1 Pending 0 1s capacity-res-job-fc8v2 1/1 Running 0 4m8s capacity-res-job-fc8v2 1/1 Terminating 0 4m8s capacity-res-job-rh8cq 1/1 Terminating 0 4m7s helloweb-75f7cfd4f7-llkfh 0/1 Pending 0 1s capacity-res-job-fc8v2 1/1 Terminating 0 4m8s capacity-res-job-n9w6p 1/1 Terminating 0 4m9s capacity-res-job-g56jm 1/1 Terminating 0 4m9s capacity-res-job-nsmks 1/1 Terminating 0 4m8s capacity-res-job-ks6j7 1/1 Terminating 0 4m8s capacity-res-job-t7h4t 1/1 Terminating 0 4m8s capacity-res-job-lzpqk 1/1 Terminating 0 4m9s capacity-res-job-t7h4t 1/1 Terminating 0 4m8s capacity-res-job-nsmks 1/1 Terminating 0 4m8s capacity-res-job-zxgg4 1/1 Terminating 0 4m8s capacity-res-job-ks6j7 1/1 Terminating 0 4m8s capacity-res-job-ph5cp 1/1 Terminating 0 4m8s capacity-res-job-n9w6p 1/1 Terminating 0 4m9s capacity-res-job-g56jm 1/1 Terminating 0 4m9s capacity-res-job-fc8v2 1/1 Terminating 0 4m9s helloweb-75f7cfd4f7-llkfh 0/1 Pending 0 2s helloweb-75f7cfd4f7-6wq75 0/1 Pending 0 2s helloweb-75f7cfd4f7-2wblr 0/1 Pending 0 2s helloweb-75f7cfd4f7-5wjvd 0/1 Pending 0 2s capacity-res-job-rh8cq 1/1 Terminating 0 4m8s capacity-res-job-lzpqk 1/1 Terminating 0 4m9s helloweb-75f7cfd4f7-5wjvd 0/1 ContainerCreating 0 2s helloweb-75f7cfd4f7-llkfh 0/1 ContainerCreating 0 2s helloweb-75f7cfd4f7-6wq75 0/1 ContainerCreating 0 2s helloweb-75f7cfd4f7-2wblr 0/1 ContainerCreating 0 2s helloweb-75f7cfd4f7-rvh8w 1/1 Running 0 9s helloweb-75f7cfd4f7-2wblr 1/1 Running 0 9s helloweb-75f7cfd4f7-6wq75 1/1 Running 0 10s helloweb-75f7cfd4f7-5wjvd 1/1 Running 0 12s helloweb-75f7cfd4f7-llkfh 1/1 Running 0 13s また、Job から作成された優先度の低いプレースホルダ Pod はすべて削除されています。 このように、Job を使用する場合は、置き換えが起こった後にプレースホルダ Pod を再作成するための容量が確保されないため、使用リソースを節約できる反面、再度スケールアウトが必要になったときに対応することができなくなります。 # 出力例 $ kubectl get po NAME READY STATUS RESTARTS AGE helloweb-75f7cfd4f7-2wblr 1/1 Running 0 3m10s helloweb-75f7cfd4f7-6wq75 1/1 Running 0 3m10s helloweb-75f7cfd4f7-cclwt 1/1 Running 0 2m helloweb-75f7cfd4f7-kw7t5 1/1 Running 0 2m helloweb-75f7cfd4f7-rvh8w 1/1 Running 0 3m10s 佐々木 駿太 (記事一覧) G-gen 最北端、北海道在住のクラウドソリューション部エンジニア。 2022 年 6 月に G-gen にジョイン。Google Cloud All Certifications Engineer。 好きな Google Cloud プロダクトは Cloud Run。最近は Dataflow を勉強中。 Follow @sasashun0805
G-gen の杉村です。Google Cloud(旧称 GCP)の VPC やオンプレミス(専用線・VPN)ネットワークの間でハブアンドスポーク構成を実現するためのフルマネージドサービスである Network Connectivity Center をご紹介します。 概要 Network Connectivity Center とは ハブアンドスポークとは ユースケース 料金 AWS Transit Gateway との違い コンポーネント 概要 スポークの種類(タイプ) 各スポークタイプの概要 オンプレミスサイトと VPC 間の接続 概要 ルート伝播 VPC 間接続(VPC スポーク) 概要 他のプロジェクトとの接続 VPC ネットワークピアリングとの違い VPC ネットワークピアリングの推移的接続の制限 プロデューサー VPC スポークと Private Service Access 制限 サイト間データ転送 概要と制限事項 経路交換とトラフィック AS 番号 Router アプライアンスを使った構成 概要 オンプレミス拠点と VPC を接続(サイト-to-クラウド接続) VPC 間接続 事前定義されたトポロジ メッシュトポロジとスタートポロジ スタートポロジの構成 監視・運用 モニタリング指標 監査ログ 補足事項・留意点 可用性 パフォーマンス サイト間データ転送における通信制御 IP アドレスの留意点 カスタムルートによる推移的通信との違い 概要 Network Connectivity Center とは Network Connectivity Center とは、Google Cloud の VPC やオンプレミス(専用線・VPN)ネットワークの間でハブアンドスポーク・アーキテクチャを構成するためのフルマネージドサービスです。 ハブには、スポークとして「VPC」「Cloud VPN」「Cloud Interconnect」「Compute Engine VM のルータ仮想アプライアンス(VPC)」を接続できます。これらで接続されたネットワークが、ハブを通して経路交換し、フルメッシュ接続ができるようになります。 参考 : Network Connectivity Center の概要 ハブアンドスポークとは ハブアンドスポーク (Hub and Spoke)とは、ネットワークのトポロジを表現する用語です。 スポークとは車輪における輻(や)の意味で、車輪の中心から輪に向けて放射状に伸びる棒のことです。 車輪の輻(や) これに見立てて、ハブとなるノードを中心にして放射状にネットワークが配置されるトポロジを ハブアンドスポークアーキテクチャ と呼びます。 ネットワークにおけるハブアンドスポーク ユースケース Network Connectivity Center を使って実現可能ないくつかのネットワーク構成をご紹介します。 以下の例では、オンプレミスサイトと複数の VPC と間の通信を実現しています。 オンプレミスサイトが複数、また VPC ネットワークが複数あっても、相互に通信を実現することが可能 です。 オンプレミスサイトと複数の VPC の通信 また以下の例では、Google Cloud の VPC である Network A と、オンプレミス拠点であるサイト A、B、C を接続してフルメッシュ通信を実現しています。対応しているリージョンであれば、サイト A、B、C が別々の大陸の異なる国に位置していても、Google のネットワークを利用して通信することが可能です。つまり、 地理的に離れたオンプレミスサイト間を、Google の強力なバックボーンネットワークを活用して相互接続する ことが可能になります。 サンプルアーキテクチャ この図におけるハブとスポークの表現の方法は、先に掲げた「ネットワークにおけるハブアンドスポーク」の図と比べて、直感的ではないかもしれません。これは「Network Connectivity Center のハブとは、パケットが実際に通過する訳ではなく、スポーク間で経路交換をさせるためにグルーピングするためのリソースである」という点に起因します。詳細は後述します。 料金 Network Connectivity Center の料金は以下の2軸で決まります。 スポークの利用料金 データ転送料金 Advanced Data Networking(ADN)料金 前者の スポークの利用料金 は、スポークが存在する時間に対する従量課金です。ただし無料枠として3つの VPN スポークと3つの Cloud Interconnect スポークは無料です。それぞれ4つめ以降から課金が発生します。スポークの種類によって単価が異なります。 後者の データ転送料金 は、Google Cloud と外部ネットワークの間で流れるデータサイズに対して課金されます。単価は地域や総データサイズによって異なりますが、例として東京リージョンの Google Cloud と日本国内の外部ネットワークの間における0〜1TiBの範囲内での単価は $0.11/GiB です(2025年4月現在)。 また Advanced Data Networking (ADN)料金は、後述する VPC スポーク間の通信のみで発生する、ハブを介して処理されたデータサイズへの課金のことです。単価は、すべてのリージョンで $0.02/GiB です(2025年4月現在)。 最新の料金表や計算例については、以下のドキュメントをご参照ください。 参考 : Pricing AWS Transit Gateway との違い 他のクラウドサービス経験者が仕様を理解しやすくするために、Amazon Web Services(AWS)の AWS Transit Gateway との違いを簡記します。 Network Connectivity Center と AWS Transit Gateway は共に複数のネットワークを接続してハブアンドスポーク構成を実現するためのフルマネージドサービスです。 AWS Transit Gateway では、Network Connectivity Center よりもより細かいルーティングの制御ができる点が異なります。 観点 Network Connectivity Center AWS Transit Gateway 対応スポーク 専用線 / VPN / VPC 専用線 / VPN / VPC ルーティング フルメッシュ接続が前提だが、広報する経路を一部制御できる AWS Transit Gateway 自体がルートテーブルを持つ。静的ルートを含む細かいルート設計が可能 課金 時間(h)とデータ容量(GiB) 時間(h)とデータ容量(GiB) コンポーネント 概要 Network Connectivity Center のコンポーネントには大きく分けて「ハブ」と「スポーク」が存在します。 ハブ は、Network Connectivity Center で構築するハブアンドスポークアーキテクチャのネットワークの中心となるコンポーネントです。 ハブは1つのプロジェクトに所属します。また、グローバルリソース(特定のリージョンに所属しないリソース)です。ハブにはほとんど設定項目が存在せず、スポークをグルーピングするためのリソースと考えて差し支えありません。 一方の スポーク は、ハブとネットワークを繋ぐリソースです。リージョンリソースであり、1つのVPC ネットワークに所属します。 参考 : Network Connectivity Center の概要 - 仕組み スポークの種類(タイプ) スポークには以下の種類(タイプ)があります。 No 名称 説明 1 VPN トンネル Cloud VPN(HA VPN)を接続するためのスポーク 2 VLAN アタッチメント Cloud Interconnect を接続するためのスポーク 3 Router アプライアンス 仮想ルータアプライアンスで確立された VPN を接続したり VPC 同士を接続するためのスポーク 4 VPC VPC を接続するためのスポーク なお、VPC タイプ以外の3つのスポークは ハイブリッドスポーク と総称されます。ハイブリッドスポークで「サイト間データ転送」オプションを有効化すると、ハイブリッドスポーク同士(オンプレミスサイト同士)での通信が可能になります。 各スポークタイプの概要 VPN トンネル タイプのスポークは、その名の通り Cloud VPN トンネルを通して外部ネットワークと接続します。なお Cloud VPN には「Classic VPN」とより新しい「HA VPN」がありますが、後者のみが利用可能です。 VLAN アタッチメント タイプも同様に、Cloud Interconnect リソースである VLAN アタッチメントを通して外部ネットワークと接続します。 Router アプライアンス は、仮想ルータのアプライアンス(Compute Engine VM)を指しています。このスポークを使うパターンは後述します。ルータアプライアンスは、Google が指定する特定のサードパーティが公開するマシンイメージからデプロイする必要があります。 VPC タイプのスポークは、その名の通り複数 VPC 間を接続するためのスポークです。他組織・他プロジェクトの VPC 同士も接続が可能です。 オンプレミスサイトと VPC 間の接続 概要 Network Connectivity Center では、1つのハブに VPC スポークとハイブリッドスポーク(Cloud VPN や Cloud Interconnect)を接続することで、 複数の VPC ネットワークと複数のオンプレミスサイトの相互通信が可能 です。 参考 : VPC スポークによるルート交換 オンプレミスサイトと VPC 間の接続 このように、Network Connectivity Center を使ってオンプレミスサイトと VPC の間の接続を構成するとき、VPC スポークを介してハブに接続する VPC のことを ワークロード VPC ネットワーク と呼びます。 また、ハイブリッドスポークとしてハブに接続されている Cloud Interconnect VLAN アタッチメントや HA VPN トンネルを持つ VPC のことは ルーティング VPC ネットワーク と呼びます。 ワークロード VPC ネットワークを「VPC スポーク」として、ルーティング VPC ネットワークを「ハイブリッドスポーク」として、それぞれハブに接続することで、相互通信が可能になります。 また同時に、ルーティング VPC ネットワークを「VPC スポーク」としてハブに接続することも可能です。これによりルーティング VPC ネットワーク内のサブネットへの経路がハブに広報されるため、他の ワークロード VPC ネットワークからルーティング VPC ネットワーク内の VM 等への接続が可能になります。 参考 : VPC スポークによるルート交換 ルート伝播 ハブに VPC スポークとハイブリッドスポークを接続すると、ハブのルートテーブルには、接続されたすべての VPC サブネットとオンプレミスサイトの動的ルートが記録されます。 ハブに新しく VPC スポークやハイブリッドスポークが接続されたり、あるいは削除されたとき、ハブのルートテーブルは自動的にルートを学習します。 ハブがハイブリッドスポークから(オンプレミス側から)学んだルートは、VPC スポークに自動的に広報されます。 ハブが VPC スポークから学習した動的ルートは、 Import of hub subnets for hybrid spokes を有効化することで、自動的にハイブリッドスポーク(オンプレミス側)に広報されます。このとき --include-import-ranges オプションで、オンプレミス側に広報する IP アドレスレンジを CIDR 形式で指定できます。 ALL_IPV4_RANGES 設定値を指定することで、すべての CIDR を広報することも可能です。 広報するルートを細かくコントロールしたいときは、Import of hub subnets for hybrid spokes を使わずに、Cloud VPN や Cloud Interconnect の Cloud Router で カスタムルート広報 を構成することで、明示的にルートを広報することも可能です。 参考 : ハイブリッド スポークのハブサブネットのインポート VPC 間接続(VPC スポーク) 概要 VPC タイプのスポーク を用いることで、VPC 間のフルメッシュ接続を実現できます。異なる組織・異なるプロジェクトの VPC ネットワーク同士を接続することも可能です。 VPC スポーク間の接続は、IPv4 アドレスと IPv6 アドレスの両方に対応しています。ハブ作成時にメッシュトポロジ(後述)を選択した場合でも、 エクスポートフィルタ により特定の IP レンジのみルート交換を除外することも可能です。これにより、特定のサブネットだけをフルメッシュ接続から除外することができます。 参考 : VPC スポークの概要 VPC スポークを使ったフルメッシュ接続 他のプロジェクトとの接続 VPC タイプのスポークでは、異なる組織・異なるプロジェクトの VPC ネットワーク同士を接続することも可能です。 ハブを持つプロジェクトは別のプロジェクトで VPC スポークが作られた場合、作成時は、スポークが非アクティブ状態になります。ハブ側の管理者が承認して初めて、スポークが有効化されます。 VPC ネットワークピアリングとの違い 異なる VPC ネットワーク同士の接続は、Network Connectivity Center を使わなくても、 VPC ネットワークピアリング を用いることで実現できます。 参考 : VPC ネットワーク ピアリング 参考 : Google CloudのVPCを徹底解説!(応用編) - G-gen Tech Blog - VPC ネットワークピアリング しかし、VPC ネットワークピアリングは VPC ネットワーク同士をピアツーピアで繋ぐ機能のため、VPC の数を n とすると n(n-1)/2 個のピアリングが必要になります。VPC の数が多くなればなるほど各種上限への抵触リスクと管理オーバヘッドが上昇します。 Network Connectivity Center の VPC スポークを使えば、VPC ネットワークをハブに接続することで容易にフルメッシュ接続を実現できます。また、VM インスタンス数の制限もありません。VPC ネットワークピアリングとの差異に関するその他の情報は以下の公式ガイドをご参照ください。 参考 : VPC スポークの概要 - VPC ネットワーク ピアリングとの比較 VPC ネットワークピアリングとの違い VPC ネットワークピアリングの推移的接続の制限 ある VPC が VPC ネットワークピアリング経由で受け取ったルートは、Network Connectivity Center へは再広報されません。以下のような構成を例に取ります。 VPC A と VPC B は、ハブに接続されている VPC C は、VPC A と VPC ネットワークピアリングで接続されている 上記のような環境では、パケットの到達可能性は以下のようになります。 通信経路 到達可能性 A <-> B True A <-> C True B <-> C False VPC ネットワークピアリングによる推移的な通信 プロデューサー VPC スポークと Private Service Access プロデューサー VPC スポーク (Producer VPC spokes)を使うと、ハブに接続された各 VPC から、 Private Service Access を使う Google Cloud サービスへの接続性を確保することができます。 Private Service Access とは、Cloud SQL や Memorystore、Cloud Build などで用いられる仕組みです。これらのサービスのインスタンスは、Google が管理する VPC ネットワークで起動します。この Google 管理 の VPC ネットワークのことを、 サービスプロデューサーネットワーク と呼びます。サービスプロデューサーネットワークとユーザーの VPC は servicenetworking-googleapis-com という名称の VPC ネットワークピアリングで接続されます。 プロデューサー VPC スポークを設定することで、ハブに接続されたユーザーの VPC からサービスプロデューサーネットワークに推移的にアクセスすることができます。構成は、以下のようになります。 Producer VPC spokes を使った通信 参考 : プロデューサー VPC スポーク 制限 代表的な仕様上の制限等を紹介します。 ハブ経由での VPC ネットワークピアリングへの推移的接続は不可 同じハブに接続する VPC ネットワーク間では VPC ネットワークピアリングを使用できない ただしプロデューサー VPC スポークを除く VPC スポーク間のルート交換をスタティックに設定することはできない 自動モードの VPC ネットワーク(デフォルト VPC など)は、VPC スポークとしてハブに接続できない 制限の詳細は、以下の公式ドキュメントをご参照ください。 参考 : VPC スポークの概要 - 制限事項 サイト間データ転送 概要と制限事項 ハイブリッドスポークで サイト間データ転送 を有効化すると、同じハブに接続されたハイブリッドスポーク同士の間で BGP による経路交換が行われ、フルメッシュの相互通信が可能になります。これにより、例えば米国のオンプレミスサイトと日本のオンプレミスサイトが、Google Cloud のネットワークを介して相互に通信することが可能になります。 ただしあるハブにおいて、オンプレミスサイト間同士のデータ転送を行うには、全てのスポークリソース(VPN トンネル、VLAN アタッチメント、アプライアンス VM)が 同じ VPC ネットワークに所属 している必要があります。 またサイト間データ転送は使えるリージョンが制限されています。対応リージョンは日本、韓国、シンガポール、米国、フランス、ドイツ、英国など一部のリージョオンのみです。リージョンの一覧は以下のドキュメントをご参照ください。 参考 : データ転送でサポートされているロケーション その他の制限事項や考慮事項は、以下を参照してください。 参考 : サイト間データ転送の概要 経路交換とトラフィック サイト間データ転送が複数スポークで有効化されると、各スポークが BGP で受け取った経路が Network Connectivity Center により全スポークに再広報されます。これによりスポーク同士がフルメッシュでトラフィックをやりとりできるようになります。 実際にパケットがハブを通るわけではなく、Google Cloud の内部ネットワークで折り返して、スポーク間で直接トラフィックがやりとりされます。 経路交換とトラフィック 参考 : サイト間データ転送によるルート交換 AS 番号 サイト間データ転送では各ネットワークは BGP を用いて経路交換するため、以下の AS 番号(ASN)の要件に従う必要があります。 単一ハブに紐づく Cloud Router は全て同じ AS 番号 各スポーク側は、単一スポーク内では同じ AS 番号 Cloud Router と各スポークは、重複しない AS 番号 AS 番号(ASN)の設計 参考 : サイト間データ転送の ASN 要件 Router アプライアンスを使った構成 概要 Router アプライアンスタイプのスポークの用途は最も分かりづらいかもしれません。以下のような用途で使われます。 オンプレミス拠点と VPC を接続(サイト-to-クラウド接続) VPC 間接続 Router アプライアンスタイプのスポークは、Compute Engine VM で動作する仮想ルータアプライアンスをベースリソースとします。 仮想ルータは Google Cloud の指定するサードパーティのイメージファイルから起動します。デプロイされた仮想ルータは Cloud Router と経路交換を行うことができ、これにより他のスポークとの経路交換を実現できます。サードパーティは Cisco、Fortinet、Palo Alto Networks などが対応しており、以下の一覧で確認できます。 参考 : Network Connectivity Center のパートナー オンプレミス拠点と VPC を接続(サイト-to-クラウド接続) オンプレミス拠点と複数 VPC の間で経路を交換し、フルメッシュ接続を行うために Router アプライアンスタイプのスポークを利用できます。 複数 VPC にまたがる Router アプライアンス Compute Engine VM は複数の VPC ネットワークにまたがってネットワークインターフェイスを持つことができます。これにより仮想ルータ VM は複数 VPC ネットワーク間でパケットのルーティングを行うことができます。 またこの図では、Router アプライアンスがオンプレミスサイトとの IPSec VPN を確立しており、オンプレミスのルータとの経路交換も行っています。仮想ルータは Cloud Router から VPC のプレフィクスを学習して、BGP を使って経路交換を行います。 参考 : サードパーティ アプライアンスを使用するサイトツークラウド トポロジ VPC 間接続 異なる複数の VPC ネットワーク間で接続を確立するために Router アプライアンスを使う方法も紹介されています。 VPC 間のトポロジ 参考 : サードパーティ アプライアンスを使用する VPC 間のトポロジ 事前定義されたトポロジ メッシュトポロジとスタートポロジ ハブを作成する際に、 事前定義されたトポロジ (Preset topologies)を選択します。選択可能なトポロジは メッシュ と スター の2種類です。トポロジの指定がない場合、デフォルトはメッシュになります。ハブの作成後にトポロジを変更することはできないため、変更したい場合は、ハブを削除して再作成する必要があります。 メッシュ トポロジは、ハブに接続されたすべてのスポークが、相互に通信可能なトポロジです。ただし、 エクスポートフィルタ を設定することで、一部のスポークをメッシュから除外することもできます。メッシュトポロジのハブにスポークが接続されると、自動的にルーティングが有効になります。 スター トポロジは、指定されたスポーク間でのみ通信できるトポロジです。スタートポロジでは、スポークは センタースポーク と エッジスポーク に分類されます。センタースポークは、他のすべてのスポークと通信できます。エッジスポークは、センタースポークに対してのみ、通信できます。 すべてのスポーク同士でフルメッシュの通信を実現したいときはメッシュトポロジを選択します。ワークロードを中央の共有 VPC ネットワーク(センタースポーク)とのみ通信させたい場合や、中央にトラフィックを集中させる仮想アプライアンスがある場合などに、スタートポロジを選択します。 参考 : VPC スポークの概要 - 事前定義されたトポロジ スタートポロジの構成 スタートポロジでは、 スポークグループ を作成します。スポークグループは、 センターグループ と、 エッジグループ の2種類にわかれます。 前述のセンタースポークとしたいスポークは、センターグループに所属させ、エッジスポークとしたいスポークをエッジグループに所属させます。 センターグループにも、エッジグループにも、複数のスポークを所属させることができます。センターグループに複数のスポークがある場合、センタースポーク同士は通信可能です。 参考 : VPC スポークの概要 - スポーク グループ なお、サイト間データ転送を有効化したハイブリッドスコープは、センターグループにのみ、所属することができます。サイト間転送が有効になっていないハイブリッドスポークであれば、センターグループにも、エッジグループにも所属することができます。 参考 : VPC スポークの概要 - 動的ルート交換の制限事項 監視・運用 モニタリング指標 Network Connectivity Center のモニタリング指標は Cloud Monitoring で自動的に取得されます。 各スポークにおける上り・下りのバイト数が主です。また各スポーク(VPC や Cloud VPN、Cloud Interconnect)の情報は、各サービス側のメトリクスを参照します。 参考 : モニタリング指標 監査ログ Network Connectivity Center からは Cloud Logging に監査ログが送信されます。設定変更(ハブやスポークの作成・更新・削除)などが記録され、Cloud Logging のメトリクスエクスプローラで閲覧することができます。 参考 : 監査ロギング情報 Cloud Logging については以下もご参照ください。 blog.g-gen.co.jp 補足事項・留意点 可用性 スポーク間を通るパケットは、実際に Network Connectivity Center のハブを通るわけではありません。Network Connectivity Center はあくまで経路交換を実現する仕組みであり、実際のパケットは Google Cloud の内部ネットワークで折り返してスポーク同士で直接やりとりされます。そのため、スポークリソース(Cloud VPN / Cloud Intercconnect / Router アプライアンス)の可用性が重要になります。 それぞれの可用性の考え方は、以下をご参照ください。 参考 : スポーク リソースの高可用性要件 また Router アプライアンスを使うケースでのアーキテクチャ例は以下のドキュメントでも示されています。 参考 : ロード バランシング型のルーター アプライアンス インスタンスを使用する Network Connectivity Center 自体はルート交換を司るため、それ自体の可用性も重要です。以下のページで SLA が公開されています。 参考 : Network Connectivity Center Service Level Agreement (SLA) パフォーマンス サイト間(スポーク同士)のデータ転送のパフォーマンスはベストエフォートであり、レイテンシや帯域は保証されません。 参考 : サイト間データ転送の概要 サイト間データ転送における通信制御 Network Connectivity Center のサイト間データ転送はフルメッシュ接続が原則です。 特定のサイトから特定の VPC への通信を制御したい場合などは、オンプレミス側であればオンプレミス側のファイアウォール、VPC 側であれば VPC ファイアウォールルール / ポリシーで、特定の IP レンジや特定プロトコル・ポート番号の通信を制限する必要があります。 Network Connectivity Center 側で特定のルートのみをフィルタするようなことはできません。 IP アドレスの留意点 ハイブリッドスポークと VPC スポークの間のルート交換は、IPv4 アドレスのみをサポートしています。VPC スポーク同士のルート交換は、IPv6 アドレスと IPv4 アドレスの両方をサポートしています。 Router アプライアンスタイプのスポークでは RFC 1918 アドレスのみがサポートされており、いわゆる Privately used public IP(PUPI)はサポートされません。 参考 : IP アドレス指定 カスタムルートによる推移的通信との違い Network Connectivity Center を使わなくても Cloud Router のカスタムルート広報機能を使えば、VPC を介した推移的通信が可能です。 ただしこの場合は、Cloud Router から広報するルートはスタティックに指定しなければいけません。Network Connectivity Center のサイト間通信の場合は、交換したルートを BGP で動的に再広報できるのが異なる点と言えます。 参考 : Cloud VPN による推移的な通信 (カスタムルート広報) 杉村 勇馬 (記事一覧) 執行役員 CTO / クラウドソリューション部 部長 元警察官という経歴を持つ現 IT エンジニア。クラウド管理・運用やネットワークに知見。AWS 12資格、Google Cloud認定資格11資格。X (旧 Twitter) では Google Cloud や AWS のアップデート情報をつぶやいています。 Follow @y_sugi_it
当記事では、 Google Cloud(旧称 GCP)リソースのプロビジョニングとオーケストレーションができるサービスである Config Controller で組織ポリシーを定義してみました。 前提知識 Config Connector Config Controller 検証概要 事前準備 API の有効化 Config Controller インスタンスの作成 Config Controller インスタンスの確認 認証情報の取得 サービスアカウントに権限を付与 マニフェストファイルの作成 マニフェストファイルの適用 動作確認 クリーンアップ 前提知識 Config Connector Config Connector とは、 Kubernetes を使用して Google Cloud リソースを管理できる open source の Kubernetes アドオンサービスです。 Kubernetes を使用するため、Kubernetes のマニフェストファイルで定義したオブジェクトを「理想の状態」として定義し、「実際の環境」に差分が生じたときに理想の状態を維持(自動修復) することができます (Reconciliation Loop) 。 その他 Kubernetes の基本については、以下のブログでも解説しています。 blog.g-gen.co.jp Config Controller Config Controller とは、Config Connector のマネージド サービスであり、実体は 限定公開の標準 GKE クラスタ が使われております。 尚、Config Controller インスタンスには Config Connector の他に、 Policy Controller と Config Sync がプリインストールされていますが、今回の検証では使用しないため説明を詳細させていただきます。 検証概要 今回は、Config Controller を用いて 組織ポリシー を定義していきたいと思います。尚、実行環境は Cloud Shell となります。 今回作成する構成 事前準備 API の有効化 必要な API を有効化します。 gcloud services enable krmapihosting.googleapis.com \ container.googleapis.com \ cloudresourcemanager.googleapis.com \ serviceusage.googleapis.com Config Controller インスタンスの作成 以下のコマンドを実行し、Config Controller インスタンスを作成します。 CONFIG_CONTROLLER_NAME={Config Controller 名を入力} LOCATION={ロケーション名を入力} gcloud anthos config controller create ${CONFIG_CONTROLLER_NAME} \ --location=${LOCATION} Config Controller インスタンスの確認 Config Controller インスタンスのリストを表示し、Config Controller インスタンスが作成されたことを確認します。 gcloud anthos config controller list \ --location=${LOCATION} STATE が RUNNING になれば、無事作成できています。 NAME: <CONFIG_CONTROLLER_NAME> LOCATION: <LOCATION> STATE: RUNNING 認証情報の取得 Config Controller インスタンスの認証情報とエンドポイント情報を取得します。 gcloud anthos config controller get-credentials ${CONFIG_CONTROLLER_NAME} \ --location ${LOCATION} サービスアカウントに権限を付与 Config Controller が Google Cloud リソースを管理するため、Config Controller のサービスアカウントに権限を付与します。 PROJECT_NO={プロジェクト番号を入力} PROJECT_ID={プロジェクト ID を入力} ORG_ID={組織 ID を入力} SA_EMAIL="service-${PROJECT_NO}@gcp-sa-yakima.iam.gserviceaccount.com" # プロジェクトに対してのオーナーロールをサービスアカウントに付与 gcloud projects add-iam-policy-binding ${PROJECT_ID} \ --member "serviceAccount:${SA_EMAIL}" \ --role "roles/owner" \ --project ${PROJECT_ID} # 組織に対しての組織ポリシー管理者ロールをサービスアカウントに付与 gcloud organizations add-iam-policy-binding ${ORG_ID} \ --role=roles/orgpolicy.policyAdmin \ --condition=None \ --member="serviceAccount:${SA_EMAIL}" マニフェストファイルの作成 Cloud Shell 環境に org_policy.yaml という名前のファイルを作成し、以下のコードを記入。尚、 ${ORG_ID} 部分は組織 ID に置き換えて入力して下さい。 apiVersion : resourcemanager.cnrm.cloud.google.com/v1beta1 kind : ResourceManagerPolicy metadata : name : resourcemanagerpolicy-sample-org spec : organizationRef : external : ${ORG_ID} constraint : "constraints/iam.disableServiceAccountCreation" booleanPolicy : enforced : true 以下に、マニフェストファイルの説明を記載します。 apiVersion や kind フィールドで作成対象となるリソースを指定します。今回は組織ポリシーを作成したいので、apiVersion に resourcemanager.cnrm.cloud.google.com/v1beta1 に kind に ResourceManagerPolicy を指定します。 metadata フィールドでは、リソースのメタデータを定義します。今回は name でリソースの名前を定義します。 spec フィールドでは、Kubernetes オブジェクトの理想状態を定義します。今回は、 サービスアカウントの作成を禁止 する組織ポリシーを、 組織 レベルで適用するよう記述しています。 マニフェストファイルの記述方法については、以下をご参照下さい。 参考: Config Connector resources 参考: ResourceManagerPolicy マニフェストファイルの適用 以下のコマンドでマニフェストファイルを適用します。 kubectl apply -f org_policy.yaml リソースのステータスを確認します。 KIND={マニフェストの kind で定義したリソースの種類を入力} NAME={マニフェストの metadata.name で定義したリソースの名前を入力} kubectl get ${KIND} ${NAME} 以下のように、STATUS のイベントタイプが UpToDate となっていれば成功です。 NAME AGE READY STATUS STATUS AGE resourcemanagerpolicy-sample-org 33m True UpToDate 33m 参考: Config Connector 固有のイベント 動作確認 コンソールから [IAM と管理] > [組織ポリシー] を選択し、フィルタに [constraints/iam.disableServiceAccountCreation] を入力すると対象のポリシーがソートされます。 組織ポリシーの設定① Config Controller のリソースとして定義したため、組織ポリシーのステータスが 適用 になっていることがわかります。 次に、この組織ポリシーのステータスを手動で [未適用] に変更し保存します。 組織ポリシーの設定② Config Connector は、 平均 10 分間隔で各リソースを調整 するためしばらく待機した後、再度組織ポリシーを確認してみます。 組織ポリシーの設定③ 自動修復されていることが確認できました。 クリーンアップ 組織ポリシーリソースを削除 kubectl delete -f org_policy.yaml Config Controller を削除 gcloud anthos config controller delete \ --location=${LOCATION} ${CONFIG_CONTROLLER_NAME} G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
当記事では、Goolge Cloud の Vertex AI でサポートされている生成 AI 機能(総称 Generative AI on Vertex AI )を解説します。 概要 Generative AI on Vertex AI とは 生成 AI とは Vertex AI Studio Gen AI SDK Vertex AI Model Garden 生成 AI モデルへのリクエスト プロンプト パラメータ アクセス制御 料金 概要 Google のスタンス 責任ある AI 安全フィルタ データガバナンス Gemini 系プロダクト 概要 Generative AI on Vertex AI とは Vertex AI は、Google Cloud の統合された機械学習プラットフォームです。Vertex AI では、生成 AI に関する諸機能もサポートされています。Gemini をはじめとする生成 AI モデルを API 経由で呼び出せたり、Web UI で簡単に AI モデルの機能を試せる Vertex AI Studio 、Claude や Llama などサードパーティのモデルを購入して呼び出せる Vertex AI Model Garden などがあります。これらの生成 AI 機能は、総称して Generative AI on Vertex AI (Vertex AI の生成 AI)と呼ばれます。 参考 : Vertex AI の生成 AI の概要 Vertex AI の Generative AI コンポーネント Vertex AI の生成 AI 関連以外の機能については、以下の記事もご参照下さい。 blog.g-gen.co.jp 生成 AI とは 生成 AI (Generative AI) とは、テキストや画像、動画などのコンテンツを生成するのに特化した AI(人工知能)の総称であり、大規模言語モデル(LLM)などのテクノロジーに基づいています。単なるテキスト生成に留まらず、ソースコードや音声、画像、動画なども生成できます。 Google が提供するコンシューマ向け生成 AI の代表的なサービスとして、 Gemini アプリ があります。 参考 : Gemini アプリ 上記の Gemini アプリは PC 向けおよびモバイル端末向けのアプリケーションであり、API が公開されておりません。開発者が自社のアプリケーションに生成 AI を組み込んだり、独自の業界知識に基づいたファインチューニングを行うためには、エンタープライズに特化して Google Cloud 経由で提供されている Generative AI on Vertex AI を利用します。 Vertex AI API 経由でのモデル利用 Vertex AI で利用可能な生成 AI モデルの代表は、 Gemini です。2025年7月現在、Gemini には Gemini 2.5 Pro、Gemini 2.5 Flash などが存在します。Gemini は数々のベンチマークで高得点を記録する、非常に優れた生成 AI モデルです。 Vertex AI Studio Vertex AI Studio は、Google Cloud の Web コンソール画面から、基盤モデルのプロンプトやパラメータ値を迅速にテストしてプロンプト設計をスムーズに行うことができる機能です。気軽に Gemini などのモデルの性能を試験することができます。 Vertex AI Studio では、Gemini のみならず、画像生成モデルである Imagen や、動画生成モデルである Veo を試用することもできます。 参考 : Vertex AI Studio で Gemini プロンプトを作成して最適化する Vertex AI Studio の Web UI この画面からは、コンソール画面で入力したプロンプトとパラメータ値が含まれた形で、Vertex AI SDK for Python などで記述されたサンプルコードや、curl で実行できる REST API のサンプルコードが表示されます。これらのサンプルコードを開発に活かすことができます。 参考 : Vertex AI SDK for Python サンプルコードの取得 Vertex AI Studio の利用手順については、以下の記事も参照してください。 blog.g-gen.co.jp Gen AI SDK Vertex AI 経由で Gemini 等のモデルを呼び出すには、各プログラミング言語用に提供されている Google Gen AI SDK を利用します。この SDK により、開発者は容易に Vertex AI 経由で生成 AI モデルを独自のアプリケーションに組み込むことができます。 Gen AI SDK は、Python、Go、Node.js、Java などの言語に対して提供されています。 参考 : Google Gen AI SDK Google Gen AI SDK を用いて、呼び出し先の Google Cloud プロジェクトやモデル名を指定することで、生成 AI モデルにリクエストを送信することができます。この SDK は個人や小規模開発者向けの生成 AI プラットフォームである Google AI Studio とも共通しています。 Vertex AI API 経由でのモデル利用 なお、以前は Vertex AI 経由で生成 AI モデルを呼び出す際に Vertex AI SDK が使われていましたが、Vertex AI SDK 経由での生成 AI モデルは2026年6月に廃止予定です。以前から Vertex AI SDK を使っている場合、Google Gen AI SDK に移行する必要があります。以下のドキュメントを参照してください。 参考 : Vertex AI SDK migration guide Vertex AI Model Garden Vertex AI Model Garden は、機械学習モデルのマーケットプレイスです。さまざまなサードパーティベンダーから公開される機械学習モデルと API のアセットを購入して、Vertex AI 経由で呼び出せるようにすることができます。 参考 : Model Garden で AI モデルを確認する Vertex AI Model Garden Model Garden で利用できるモデルカテゴリは以下のとおりです。 No モデルカテゴリ 説明 1 基盤モデル Vertex AI Studio、Vertex AI API、Vertex AI SDK などを使用して、特定のタスクに合わせて調整またはカスタマイズできる、事前トレーニングされた基盤モデル 2 ファインチューニング可能なモデル カスタムノートブックまたはパイプラインを使用して微調整できるモデル 3 タスク固有のソリューション すぐに使用可能な事前構築済みモデル 生成 AI モデルへのリクエスト プロンプト プロンプト とは、簡単に言うと言語モデルに送信するリクエストのことです。 プロンプトには、質問だけでなく、コンテキストや指示、例などを含めることができ、モデルはプロンプトを受け取ると、テキストやソースコード、画像等を生成しユーザーにレスポンスします。 特に、大規模言語モデルは膨大な量のテキストデータから単語間のパターンと関係を学習しており、プロンプトの設計がうまくできればモデルの精度を向上させることができます。 プロンプトをより良くするためには、「目的」「指示」「出力形式」を明記したり、「システム指示(system instruction)」を用います。またモデルの振る舞いを「役割(ペルソナ)」として示したり、出力の例を数個提示する Few-shot prompting などの技術が知られています。 以下のドキュメントでは、より良いプロンプトを設計するための戦略が紹介されています。 参考 : プロンプトの概要 参考 : プロンプト戦略の概要 参考 : カスタム Gem 作成のヒント 公式ドキュメントにプロンプトのサンプルが提供されているので、まずはこちらのサンプルからニーズにあうプロンプトがあるか探してみることをおすすめします。 参考 : 生成 AI のプロンプト サンプル パラメータ モデルに送信するリクエストには、モデルのレスポンスを制御する パラメータ を含めることができます。代表的なパラメータ値を以下に記載します。 No パラメータ値 概要 1 Max output tokens モデルが生成できるトークンの最大数です。参考値として、1 トークンは約 4 文字、100 トークンは約 60 ~ 80 英単語に相当。 2 Top-K モデルが最適なトークンを選択する際、最も確率の高い上位 K 個のトークンがサンプリングされます。 3 Top-P 確率の高い順にトークンを並べ、確率の合計が上位 P の値に等しくなるまでフィルタリングされます。Top-P は 0.0 ~ 1.0 の値をとります。 4 Temperature Temperature は、トークン選択のランダム性を制御し、0.0 ~ 1.0 の値を取ります。Temperature が低い (0.0 に近い) と、確率の高いトークン、つまり、より真実または正しいレスポンスが生成されます。逆に Temperature が高い (1.0 に近い) と、より多様な結果や予期しない結果を得ることができます。 モデルに送信されるリクエストに対して、最適なトークンを選択するステップは以下のとおりです。 Top-K により最も高い確率を持つ上位 K 個のトークンがサンプリング Top-K でサンプリングされた K 個のトークンから、Top-P に基づいてフィルタリング Top-P でフィルタリングされたトークンから、Temperature に基づいて最終的にトークンが選択される 例えば、アイディア出しを行いたい場合、予期しない結果を得たいため Top-K と Top-P、Temperature を上げてみるといいでしょう。 参考: パラメータ値を試す アクセス制御 Vertex AI 経由で生成 AI 機能を使用するには、呼び出し元の Google アカウントまたは Google グループ、あるいはサービスアカウント等に、適切な権限が付与されている必要があります。 以下の事前定義されたロールを付与することで、Vertex AI で Generative AI 機能へのアクセスを許可できます。 Vertex AI 管理者( roles/aiplatform.admin ) Vertex AI ユーザー( roles/aiplatform.user ) 参考: アクセス制御 料金 概要 Vertex AI での生成 AI の課金体系は、入力や出力のボリュームに応じた従量課金です。入出力のボリュームは、モデルにより文字数、または トークン という単位で計測されます。 例として、2025年7月現在、Gemini 2.5 Pro の課金体系は以下です(20万以下のコンテキストウインドウの入力の場合)。入力した画像、動画、テキストの量と、出力された生成コンテンツの量に応じた従量課金となります。入出力のサイズは、トークンと呼ばれる単位でカウントされます。 課金軸 料金単価 入力トークン $1.25 / 100万トークン 出力トークン (テキスト) $10 / 100万トークン モデルによって料金単価や計測方法が異なるため、詳細は以下の公式ページを参照してください。 参考 : Vertex AI での AI モデルの構築とデプロイにかかる費用 Google のスタンス 責任ある AI Gemini 等の Google が公開するモデルは、 Google の AI 原則 に従って設計されています。 参考 : Our AI Principles しかしながら、生成 AI の生成は非決定論的であり、誤った情報や不適切なコンテンツを生成してしまう可能性はゼロではありません。開発者はこれらのリスクを考慮しつつ、安全かつ責任を持ってテスト・デプロイを行うことが重要です。 安全フィルタ Vertex AI 経由での Gemini モデル等の呼び出し時には、安全フィルタのしきい値を設定することで、基盤モデルから有害なレスポンスが返ってくる可能性を調整できます。 ヘイトスピーチ、嫌がらせ、性的に露骨な表現、危険なコンテンツなどに対してフィルタを設定し、フィルタの強度も指定可能です。 参考 : 安全フィルタを構成する データガバナンス Google Cloud 経由で提供される生成 AI モデルでは、入出力データは保護されます。サービス規約上、Google はユーザーのデータを利用して、AI/ML モデルを再トレーニングしたり、ファインチューニングすることはありません。 参考 : 生成 AI とデータ ガバナンス なお、Google は AI/ML Privacy Commitment を業界で初めて公表した企業です。 参考 : Sharing our data privacy commitments for the AI era Gemini 系プロダクト Google Cloud 等で提供される Gemini 系プロダクトの一覧については、以下の記事も参照してください。 blog.g-gen.co.jp G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
G-gen の武井です。当記事では Cloud Monitoring アラートを使って VM マシンのメトリクスを監視をする方法を紹介します。 はじめに 前提知識 Cloud Monitoring とは 指標の収集 アラート 設定手順 概要 通知先の登録 Slack のメールアドレスを取得 通知チャネル (notification channel) の作成 アラートポリシーの作成 指標 フィルタ ローリングウィンドウ / ローリングウィンドウ関数 トリガー アラート通知メール件名の規則 通知チャネル 動作確認 アラートポリシーの確認 インシデントの確認 通知の確認 関連機能1 : 繰り返し通知 概要 設定方法 概要 JSON ファイルの準備 NotificationChannelStrategy オブジェクトの定義 アラートポリシーの更新 設定内容の確認 動作確認 注意事項 関連機能2: スヌーズ 概要 設定方法 概要 JSON ファイルの準備 スヌーズの作成 設定内容の確認 動作確認 はじめに Google Cloud では、 Cloud Operations (オペレーション スイート) という名称でインフラやアプリケーションの監視を行うための一連のプロダクトを提供しています。 これらはマネージドサービスとして提供されており、Google Cloud をはじめ、他のパブリッククラウドやオンプレミス環境の情報を収集して監視や運用に活用することができます。 当記事では Cloud Monitoring の アラート 機能を使って VM マシンのメトリクスを監視をする方法を紹介します。 参考 : アラートの仕組み 前提知識 Cloud Monitoring とは オペレーションスイートの 1つ である Cloud Monitoring は次の機能を提供します。 指標 (メトリクス) の収集 可視化 (ダッシュボード) アラート管理 インシデント管理 詳細は以下の記事で解説しておりますのでご参照ください。 blog.g-gen.co.jp 指標の収集 Google Cloud の各種リソースから自動的に収集される標準的な指標を Google Cloud の指標 といいますが、この指標には メモリ使用率 、 ディスク使用率 、 スワップ使用率 といった VM マシンにおける重要な指標が含まれていません。 Ops エージェント とよばれる fluentbit ベースのエージェントソフトウェアをインストールすると、 Ops エージェントの指標 として収集できます。 ※当記事では Ops エージェントのインストール方法に関する説明は割愛します。 アラート 指標にしきい値を設定し、超過した際にメールを飛ばすなどの アラート設定 が行えます。 一つ一つの設定を アラートポリシー といい、設定内容は大きく分けて次の 2つ です。 # 設定内容 説明 1 条件 監視対象となる指標、リソース、トリガーなどを定義 2 通知 通知先 (メール、SMS、Slack 等) やアラート名などを定義 これにより、例えば「CPU使用率が」「あるインスタンスグループで」「5分の間」「80%を超えた場合に」「メールを発報する」 といったアラートを制御できます。 設定手順 概要 今回は Linux VM マシン 2台 を用意して、各マシンのディスク使用率がしきい値を超過した際に チャットツール「Slack」にアラートを通知させます。 通知先の登録 Slack のメールアドレスを取得 今回は Slack をアラートの通知先として使用します。 こちら に従い任意のチャネルに紐づくメールアドレスを取得します。 ※ Slack チャンネルのインテグレーション用メールアドレスを取得するには Slack の有料プランの契約が必要です。 通知チャネル (notification channel) の作成 先程取得した Slack 通知用のメールアドレスを Monitoring アラートの通知先として登録します。Cloud Monitoring ではアラートの通知先を 通知チャネル といいます。 Cloud コンソール > Monitoring > アラート の順に遷移し、 EDIT NOTIFICATION CHANNELS をクリックします。 EDIT NOTIFICATION CHANNEL をクリック 次に Email の ADD NEW をクリックし、メールアドレスを登録します。 ADD NEW をクリック メールアドレスと表示名を入力して保存 通知チャネルとして登録 アラートポリシーの作成 ディスク使用率の指標を使ってアラートを設定します。 Cloud コンソール > Monitoring > アラート の順に遷移し、 CREATE POLICY をクリックします。 CREATE POLICY をクリック 指標 指標を選択 をクリックし、 VM Instance > Disk > Disk utilization を選択したら 適用 をクリックします。 指標を選択したら 適用 をクリック フィルタ Linux VM マシン 2台 のディスク (/dev/sda1) の使用率を指標とするため、 ADD A FILTER をクリックして 2つ のフィルタを作成します。 device (ボリューム名) が /dev/sda1 state (状態) が used ローリングウィンドウ / ローリングウィンドウ関数 Monitoring アラートでは ローリングウインドウ / ローリングウィンドウ関数 で定めた期間内の指標を収集・計算した上で異常か否かを判断します。 今回は過去 5分間 で収集した指標の平均値 (mean) にもとづき判断するよう設定します。 過去5分間で収集した指標の平均値を計算する トリガー トリガーはアラートの発報条件になります。 今回準備した VM マシンのディスク使用率がいずれも 23 % となっているので、 20% を超過した場合にアラートが通知 されるようにトリガーを設定します。 ディスク使用率が20%を超過した際にアラート通知 なお、 条件名 は以下に示したとおりアラート通知メールの件名として使用されるため、件名が長くなりすぎないに工夫すると良いでしょう。 アラート通知メール件名の規則 ALERT + [条件名] + on + [プロジェクト名] + [インスタンス名] 通知チャネル 最後に通知チャネルを選択し、アラートポリシー名を入力します。 通知チャネルを選択 アラートポリシー名を入力してポリシーを作成する 動作確認 アラートポリシーの確認 先ほど作成したアラートポリシーを確認します。 Cloud コンソール > Monitoring > アラート の順に遷移します。 ポリシー名をクリックすると設定内容や監視状況が確認できます。 ポリシー名をクリック 設定内容や監視状況が確認できる インシデントの確認 アラート通知の条件を満たすと、インシデントとしてダッシュボード上に登録されます。インシデントの概要名をクリックすると詳細が確認できます。 インシデントとして登録 インシデント詳細画面 通知の確認 上記同様アラート通知の条件を満たしたため、Slack に Email 通知が届いています。 Slack にアラートが通知 関連機能1 : 繰り返し通知 概要 繰り返し通知 を設定すると、インシデント登録されたアラートに対して定期的にリマインダーを送信できます。 デフォルトではインシデントが登録された際に1回だけ通知されます。サービス障害に直結する重要な監視項目に対して設定すると対応漏れなどを防ぐことができます。 繰り返し通知は Cloud コンソールからは設定できません。 詳細は次で説明しますが、アラートポリシーの AlertStrategy オブジェクトに少なくとも 1 つの NotificationChannelStrategy オブジェクトを gcloud コマンド または API から設定します。 # PROJECT_ID と CHANNEL_ID には環境固有の値を入力する # renotifyInterval には 30分以上24時間以内の値を秒単位で入力する " alertStrategy ": { " notificationChannelStrategy ": [ { " notificationChannelNames ": [ " projects/PROJECT_ID/notificationChannels/CHANNEL_ID " ] , " renotifyInterval ": " 1800s " } ] } 設定方法 概要 これまでの解説で作成したアラートポリシーを例に、 gcloud コマンドを使った繰り返し通知の設定方法を説明します。 JSON ファイルの準備 作成済みのアラートポリシーから JSON ファイルを取得します。 Cloud コンソール > Monitoring > アラート から対象のアラートポリシーを選択し、 JSON をクリックしてファイルをダウンロードします。 JSON ファイルをダウンロードする NotificationChannelStrategy オブジェクトの定義 次に先程ダウンロードした JSON ファイルの AlertStrategy オブジェクトに NotificationChannelStrategy オブジェクトを追記します。 実行例では Slack に 30分おきにリマインダーを通知するよう定義しています。 また、値が設定されていないオブジェクト (以下の場合 documentation と userLabels ) が残っているとコマンド実行時にエラーとなるので削除します。 # 変更前 { " name ": " projects/example/alertPolicies/1111111111 ", " displayName ": " Linux VM Disk Utilization Test Policy ", " documentation ": {} , " userLabels ": {} , " conditions ": [ { " name ": " projects/example/alertPolicies/1111111111/conditions/2222222222 ", " displayName ": " Disk Used (%) ", " conditionThreshold ": { " aggregations ": [ { " alignmentPeriod ": " 300s ", " perSeriesAligner ": " ALIGN_MEAN " } ] , " comparison ": " COMPARISON_GT ", " duration ": " 0s ", " filter ": " resource.type = \" gce_instance \" AND metric.type = \" agent.googleapis.com/disk/percent_used \" AND (metric.labels.device = \" /dev/sda1 \" AND metric.labels.state = \" used \" ) ", " thresholdValue ": 20 , " trigger ": { " count ": 1 } } } ] , " alertStrategy ": { " autoClose ": " 1800s " } , " combiner ": " OR ", " enabled ": true , " notificationChannels ": [ " projects/example/notificationChannels/3333333333 " ] , " creationRecord ": { " mutateTime ": " 2023-05-04T14:38:46.558191920Z ", " mutatedBy ": " example@g-gen.co.jp " } , " mutationRecord ": { " mutateTime ": " 2023-05-17T03:14:54.109178339Z ", " mutatedBy ": " example@g-gen.co.jp " } } # 変更後 { " name ": " projects/example/alertPolicies/1111111111 ", " displayName ": " Linux VM Disk Utilization Test Policy ", " conditions ": [ { " name ": " projects/example/alertPolicies/1111111111/conditions/2222222222 ", " displayName ": " Disk Used (%) ", " conditionThreshold ": { " aggregations ": [ { " alignmentPeriod ": " 300s ", " perSeriesAligner ": " ALIGN_MEAN " } ] , " comparison ": " COMPARISON_GT ", " duration ": " 0s ", " filter ": " resource.type = \" gce_instance \" AND metric.type = \" agent.googleapis.com/disk/percent_used \" AND (metric.labels.device = \" /dev/sda1 \" AND metric.labels.state = \" used \" ) ", " thresholdValue ": 20 , " trigger ": { " count ": 1 } } } ] , " alertStrategy ": { " autoClose ": " 1800s ", " notificationChannelStrategy ": [ { " notificationChannelNames ": [ " projects/example/notificationChannels/3333333333 " ] , " renotifyInterval ": " 1800s " } ] } , " combiner ": " OR ", " enabled ": true , " notificationChannels ": [ " projects/example/notificationChannels/3333333333 " ] , " creationRecord ": { " mutateTime ": " 2023-05-04T14:38:46.558191920Z ", " mutatedBy ": " example@g-gen.co.jp " } , " mutationRecord ": { " mutateTime ": " 2023-05-17T03:14:54.109178339Z ", " mutatedBy ": " example@g-gen.co.jp " } } アラートポリシーの更新 JSON ファイルの修正が完了したら、 gcloud alpha monitoring policies update コマンドで既存のアラートポリシーを更新し、繰り返し通知設定を反映します。 # アラートポリシー ID と JSON ファイルパスを指定する gcloud alpha monitoring policies update projects/example/alertPolicies/1111111111 \ --policy-from-file=example.json 設定内容の確認 gcloud alpha monitoring policies list コマンドで設定変更前後を比較します。 AlertStrategy オブジェクトに NotificationChannelStrategy オブジェクトが追加されていることがわかります。 # 変更前 [ { " alertStrategy ": { " autoClose ": " 1800s " } , " combiner ": " OR ", " conditions ": [ { " conditionThreshold ": { " aggregations ": [ { " alignmentPeriod ": " 300s ", " perSeriesAligner ": " ALIGN_MEAN " } ] , " comparison ": " COMPARISON_GT ", " duration ": " 0s ", " filter ": " resource.type = \" gce_instance \" AND metric.type = \" agent.googleapis.com/disk/percent_used \" AND (metric.labels.device = \" /dev/sda1 \" AND metric.labels.state = \" used \" ) ", " thresholdValue ": 20.0 , " trigger ": { " count ": 1 } } , " displayName ": " Disk Used (%) ", " name ": " projects/example/alertPolicies/1111111111/conditions/2222222222 " } ] , " creationRecord ": { " mutateTime ": " 2023-05-04T14:38:46.558191920Z ", " mutatedBy ": " example@g-gen.co.jp " } , " displayName ": " Linux VM Disk Utilization Test Policy ", " enabled ": true , " mutationRecord ": { " mutateTime ": " 2023-05-17T03:14:54.109178339Z ", " mutatedBy ": " example@g-gen.co.jp " } , " name ": " projects/example/alertPolicies/1111111111 ", " notificationChannels ": [ " projects/example/notificationChannels/3333333333 " ] } ] # 変更後 [ { " alertStrategy ": { " autoClose ": " 1800s ", " notificationChannelStrategy ": [ { " notificationChannelNames ": [ " projects/example/notificationChannels/3333333333 " ] , " renotifyInterval ": " 1800s " } ] } , " combiner ": " OR ", " conditions ": [ { " conditionThreshold ": { " aggregations ": [ { " alignmentPeriod ": " 300s ", " perSeriesAligner ": " ALIGN_MEAN " } ] , " comparison ": " COMPARISON_GT ", " duration ": " 0s ", " filter ": " resource.type = \" gce_instance \" AND metric.type = \" agent.googleapis.com/disk/percent_used \" AND (metric.labels.device = \" /dev/sda1 \" AND metric.labels.state = \" used \" ) ", " thresholdValue ": 20.0 , " trigger ": { " count ": 1 } } , " displayName ": " Disk Used (%) ", " name ": " projects/example/alertPolicies/1111111111/conditions/2222222222 " } ] , " creationRecord ": { " mutateTime ": " 2023-05-04T14:38:46.558191920Z ", " mutatedBy ": " example@g-gen.co.jp " } , " displayName ": " Linux VM Disk Utilization Test Policy ", " enabled ": true , " mutationRecord ": { " mutateTime ": " 2023-06-28T08:57:23.371148479Z ", " mutatedBy ": " example@g-gen.co.jp " } , " name ": " projects/example/alertPolicies/1111111111 ", " notificationChannels ": [ " projects/example/notificationChannels/3333333333 " ] } ] 動作確認 繰り返し通知設定後、30分おきにリマインダー通知を受信していることがわかります。 繰り返し通知設定により30分おきにリマインダー通知を受信 注意事項 繰り返し通知設定を実装した場合、それ以降の更新を Cloud コンソールから行うと 繰り返し通知設定が削除 されてしまうのでご注意ください。 先にもお伝えした通り、Cloud コンソールでは設定できない項目となっているため、 設定なし で上書きされてしまうからです。 関連機能2: スヌーズ 概要 スヌーズ とは、指定した期間内でインシデントの登録やアラート通知を抑制する機能です。メンテナンス中やフラッピングによって発生する不要なアラート通知を抑制する際など、様々な場面で活用できます。 長らくプレビュー状態にあった本機能ですが、ついに 2023年5月より GA (一般公開) されました。 繰り返し通知のような設定上の制約はありません。Cloud コンソール、gcloud コマンド または API から設定可能です。 設定方法 概要 今回は gcloud コマンドを使った設定方法を説明します。スヌーズ対象のアラートポリシーは前述で繰り返し通知設定を実装したポリシーとします。 Cloud コンソールや API による設定方法については こちら の公式ガイド、またはプレビュー期間中に弊社社員が執筆した以下の記事も参照いただけると幸いです。 blog.g-gen.co.jp JSON ファイルの準備 こちら の公式ガイドから取得した雛形をベースに JSON ファイルを作成します。 # スヌーズ対象のアラートポリシーはアラートポリシー ID で指定する # 開始 / 終了時刻は ISO 8601 形式 かつ UTC で指定する { " criteria ": { " policies ": [ " projects/example/alertPolicies/1111111111 " ] } , " interval ": { " startTime ": " 2023-07-03T12:15:00.000Z ", " endTime ": " 2023-07-03T14:15:00.000Z " } , " displayName ": " snooze_for_maintanance_20230703 " } スヌーズの作成 JSON ファイルの準備が完了したら、 gcloud monitoring snoozes create コマンドを実行してスヌーズを作成します。 # --snooze-from-file オプションで JSON ファイルのパスを指定する gcloud monitoring snoozes create --snooze-from-file=snooze_for_maintanance_20230703.json 成功すると以下の戻り値が表示されます。 Created snooze [projects/example/snoozes/4444444444]. 設定内容の確認 gcloud monitoring snoozes list コマンドで設定内容を確認します。 2023/07/03 21:15 ~ 23:15 (JST) の2時間の間、指定したアラートポリシーに関するアラート通知を抑制するスヌーズが作成できました。 gcloud monitoring snoozes list -- format = json [ { " criteria ": { " policies ": [ " projects/example/alertPolicies/1111111111 " ] } , " displayName ": " snooze_for_maintanance_20230703 ", " interval ": { " endTime ": " 2023-07-03T14:15:00Z ", " startTime ": " 2023-07-03T12:15:00Z " } , " name ": " projects/example/snoozes/4444444444 " } ] Cloud コンソール > Monitoring > アラート > Snoozes 上でも同様に上記スヌーズの確認が可能です。 Cloud コンソールから見たスヌーズ設定 Cloud コンソールから見たスヌーズ設定 動作確認 スヌーズ期間中はこれまで30分おきに受信していた通知が抑制されたのと合わせて、インシデントのステータスも RESOLVED (復旧) に遷移しています。 Cloud コンソール > Monitoring > アラート > Incidents 上でも確認できます。 スヌーズにより通知が抑制 スヌーズ期間中はインシデントの状態が復旧 スヌーズ期間終了後ですが、インシデントの状態が再び ALERT 状態に遷移し、アラート通知も再開しました。 スヌーズ期間終了後、アラート通知が再開 スヌーズ期間終了後にインシデントとして再登録 武井 祐介 (記事一覧) クラウドソリューション部クラウドエンジニアリング課。 Google Cloud Partner Top Engineer 2025 選出。 趣味はロードレースやサッカー観戦、あとはゴルフと筋トレ。 Follow @ggenyutakei
G-gen の佐々木です。当記事では Google Cloud (旧称 GCP) のサーバーレスなコンテナサービスである Cloud Run の タグ付きリビジョン (tagged revision)機能を解説します。 Cloud Run とは タグ付きリビジョンとは タグ付きリビジョンを使用する Cloud Run サービスのデプロイ 使用するコード(Go) コンテナイメージのビルド サービスのデプロイ サービスへのアクセス タグ付きリビジョンのデプロイ 使用するコード(Go) 新しいコンテナイメージのビルド タグ付きリビジョンのデプロイ タグ付きリビジョンへのアクセス トラフィックの移行 タグ付きリビジョンへのトラフィック移行 タグの削除 Cloud Run とは Cloud Run は、Google Cloud のサーバーレスな基盤でコンテナアプリケーションを実行できるサービスです。Cloud Run の中でも HTTP リクエストをトリガーとするものは Cloud Run services といい、コンテナとサーバーレスの利点を活かしたスケーラビリティの高い Web アプリケーション実行基盤として非常に有用なサービスとなっています。 Cloud Run services の詳細については以下の記事をご一読ください。 blog.g-gen.co.jp タグ付きリビジョンとは Cloud Run service でデプロイしたコンテナアプリケーションは、 リビジョン という単位でバージョン管理されます。Cloud Run ではリビジョンに対するトラフィック分割機能により、新旧のリビジョンに対して一定割合でトラフィックをロードバランスし、新しいリビジョンを段階的にロールアウトすることができます。 デプロイしたリビジョンに対しては、タグを付与することができます。タグ付きリビジョンには タグを含む URL が発行され、 トラフィックを新しいリビジョンにルーティングすることなくアクセスすることができるようになります。 これにより、Cloud Run 環境で新しいリビジョンのテストを行い、テストが終わったらそのままトラフィックをルーティングすることができます。 # Cloud Run サービスの通常の URL 例 https://servicename-xxxxxxxxxx-an.a.run.app # タグ付きリビジョンの URL 例(リビジョンに dev タグを付与した場合) https://dev---servicename-xxxxxxxxxx-an.a.run.app 参考: テスト、トラフィックの移行、ロールバックにタグを使用する タグ付きリビジョンを使用する Cloud Run サービスのデプロイ 使用するコード(Go) 当記事では、公式ドキュメントの クイックスタート のコードをベースにし、Cloud Run サービスをデプロイします。 package main import ( "fmt" "log" "net/http" "os" ) func main() { log.Print( "starting server..." ) http.HandleFunc( "/" , handler) // Determine port for HTTP service. port := os.Getenv( "PORT" ) if port == "" { port = "8080" log.Printf( "defaulting to port %s" , port) } // Start HTTP server. log.Printf( "listening on port %s" , port) if err := http.ListenAndServe( ":" +port, nil ); err != nil { log.Fatal(err) } } func handler(w http.ResponseWriter, r *http.Request) { s := "Hello, World!" fmt.Fprintf(w, "%s \n " , s) // ブラウザに文字列を表示する } コンテナイメージのビルド Cloud Run にデプロイするため、コンテナイメージをビルドして Artifact Registry にプッシュします。ここでは Dockerfile を使用せず、Buildpack を使用してコンテナイメージをビルドします。 イメージの新旧バージョンを分かりやすくするため、コンテナイメージには v1.0 タグを付与します。 # Buildpack を使用してイメージをビルドする $ gcloud builds submit --pack image={リポジトリの URL}/{コンテナイメージ名}:v1.0 # 実行例(リポジトリに Artifact Registry を使用) $ gcloud builds submit --pack image=asia-northeast1-docker.pkg.dev/myproject/myrepo/sample-service:v1.0 参考①: Google Cloud の Buildpack 参考②: Cloud Run で Go ジョブをビルドして作成する サービスのデプロイ 以下のコマンドを使用して Cloud Run サービスの最初のリビジョンをデプロイします。 # Cloud Run サービスのデプロイ $ gcloud run deploy {サービス名} \ --image {コンテナイメージのURL} \ --region {リージョン} \ --allow-unauthenticated # 実行例(v1.0のコンテナイメージを指定) $ gcloud run deploy sample-service \ --image asia-northeast1-docker.pkg.dev/myproject/myrepo/sample-service:v1.0 \ --region asia-northeast1 \ --allow-unauthenticated サービスへのアクセス サービスのデプロイ後に URL が出力されるので、ブラウザからサービスの URL にアクセスします。 # デプロイ後の出力抜粋 Service [sample-service] revision [sample-service-00001-xuh] has been deployed and is serving 100 percent of traffic. Service URL: https://sample-service-ai4qoprwhq-an.a.run.app 最初のリビジョンでは、ブラウザ上に「Hello, World!」の文字列が表示されます。 最初のリビジョンへのアクセスを確認する タグ付きリビジョンのデプロイ 使用するコード(Go) 最初のリビジョンと区別するため、新しいリビジョンではブラウザに表示する文字列を変更します。 当記事では、新しいリビジョンにアクセスすると「Hello, G-gen!」の文字列が表示されるようにします。 package main import ( "fmt" "log" "net/http" "os" ) func main() { log.Print( "starting server..." ) http.HandleFunc( "/" , handler) // Determine port for HTTP service. port := os.Getenv( "PORT" ) if port == "" { port = "8080" log.Printf( "defaulting to port %s" , port) } // Start HTTP server. log.Printf( "listening on port %s" , port) if err := http.ListenAndServe( ":" +port, nil ); err != nil { log.Fatal(err) } } func handler(w http.ResponseWriter, r *http.Request) { s := "Hello, G-gen!" // ここを修正する fmt.Fprintf(w, "%s \n " , s) } 新しいコンテナイメージのビルド 最初のリビジョン同様に、コンテナイメージをビルドして Artifact Registry にプッシュします。 新しいイメージには v2.0 タグを付与します。 # Buildpack を使用してイメージをビルドする $ gcloud builds submit --pack image={リポジトリの URL}/{コンテナイメージ名}:v2.0 # 実行例(リポジトリに Artifact Registry を使用) $ gcloud builds submit --pack image=asia-northeast1-docker.pkg.dev/myproject/myrepo/sample-service:v2.0 タグ付きリビジョンのデプロイ gcloud run deploy コマンドで --tag オプションを使用することで、タグ付きリビジョンをデプロイすることができます。ここで --no-traffic オプションを指定することで、新しいリビジョンにトラフィックがルーティングされないようにします。 既存のサービスに対して新しいリビジョンをデプロイするため、 サービス名 には最初に作成したサービスと同じ名前を使用します。 # タグ付きリビジョンをデプロイする $ gcloud run deploy {サービス名} \ --image {コンテナイメージのURL} \ --region {リージョン} \ --no-traffic \ --tag {タグ名} # 実行例(v2.0のコンテナイメージを指定し、「dev」タグを付与) $ gcloud run deploy sample-service \ --image asia-northeast1-docker.pkg.dev/myproject/myrepo/sample-service:v2.0 \ --region asia-northeast1 \ --no-traffic \ --tag dev タグ付きリビジョンへのアクセス タグ付きのリビジョンをデプロイするとタグが含まれる URL が発行されるので、ブラウザで URL にアクセスします。 タグ付きリビジョンの URL には、サービスの本来の URL に dev--- のような形式でタグが付与されています。 # タグ付きリビジョンのデプロイ後の出力抜粋 Service [sample-service] revision [sample-service-00002-caw] has been deployed and is serving 0 percent of traffic. The revision can be reached directly at https://dev---sample-service-ai4qoprwhq-an.a.run.app コンソールからタグをクリックすることでもアクセスすることが可能です。 コンソールからタグ付きリビジョンの URL にアクセスする 「Hello, G-gen!」が表示されているため、トラフィックがルーティングされていない新しいリビジョンにアクセスできていることがわかります。 タグ付きリビジョンへのアクセスを確認する タグがついていないサービスの URL にアクセスすると、現在トラフィックがルーティングされている最初のリビジョンにアクセスできるため、新しいリビジョンがまだ公開されていないことがわかります。 サービスの URL から最初のリビジョンにアクセスできることを確認する トラフィックの移行 タグ付きリビジョンへのトラフィック移行 タグ付きリビジョンのテストが終わったら、最初のリビジョンからトラフィックを移行します。 gcloud run services update-traffic コマンドの --to-tags オプションでタグ名を指定し、トラフィックを何パーセント割り当てるかを指定します。 # タグ付きリビジョンにトラフィックをルーティングする $ gcloud run services update-traffic {サービス名} \ --region {リージョン} \ --to-tags {タグ名}={ルーティングするトラフィックの割合} # 実行例(dev タグが付与されたリビジョンにトラフィックを 100% ルーティングする) $ gcloud run services update-traffic sample-service \ --region asia-northeast1 \ --to-tags dev=100 タグ付きの新しいリビジョンにトラフィックが 100% ルーティングされている サービスの URL にアクセスすると、新しいリビジョンにアクセスできるようになっています。 サービスの URL からタグ付きの新しいリビジョンにアクセスできることを確認する タグの削除 タグが不要になったら --remove-tag オプションでタグを指定し、サービスを更新します。 # リビジョンからタグを削除する $ gcloud run services update-traffic {サービス名} \ --region {リージョン} \ --remove-tags {タグ名} # 実行例(dev タグを削除) $ gcloud run services update-traffic sample-service \ --region asia-northeast1 \ --remove-tags dev 佐々木 駿太 (記事一覧) G-gen最北端、北海道在住のクラウドソリューション部エンジニア 2022年6月にG-genにジョイン。Google Cloud Partner Top Engineer 2024に選出。好きなGoogle CloudプロダクトはCloud Run。 趣味はコーヒー、小説(SF、ミステリ)、カラオケなど。 Follow @sasashun0805
当記事は みずほリサーチ&テクノロジーズ × G-gen エンジニアコラボレーション企画 で執筆されたものです。 G-gen の片岩です。当記事では Google Cloud のデータベースサービスである Bigtable を徹底解説します。ビジネスにおいてデータ活用が重要なことは改めて記載するまでもありません。大量のデータを高速に処理でき、スケーラビリティのある Bigtable は、より効率的かつ正確なビジネス上の意思決定に貢献できそうです。また、高度で詳細な監査要件の求められる金融機関のシステムにおいてはログの蓄積・解析などでの利用も考えられそうです。 基本事項 Cloud Bigtable とは ユースケース 料金 概要 コンピューティング料金 ストレージ料金 ネットワーク料金 計算例 データの操作 概要 クライアントライブラリ SQL サポート cbt CLI 他の Google Cloud サービス コンポーネント 全体像 インスタンス クラスタ ノード ストレージ 複数クラスタとレプリケーション 概要 レイテンシと整合性 複数クラスタのユースケース 可用性向上 オンラインとバッチの分離 グローバルなレイテンシ短縮 バックアップ・リストア 内部構造 ストレージモデル インフラ・アーキテクチャ スキーマ設計 スキーマ設計のポイント スキーマ設計のベストプラクティス スキーマ設計例 測定するごとに行を追加するパターン 測定するごとに列を追加するパターン 測定するごとにセルを追加するパターン アプリプロファイル パフォーマンスに関する注意点 概要 書き込み 読み取り モニタリング リソース使用状況の確認 Key Visualizer 基本事項 Cloud Bigtable とは Cloud Bigtable (以降、Bigtable) は NoSQL ビッグデータ向けのフルマネージドなデータベースサービスです。 特徴はなんと言っても 低レイテンシかつ高スループット であることで、 膨大なデータをリアルタイムで処理する ことが可能です。 Bigtable は Google 検索、Google Analytics、Google マップ、Gmail など、Google の主要サービスを支えているサービスでもあります。 Bigtable は NoSQL データベースであり、行 (Key) と列 (Value) で構成される Key-Value マップにデータを格納します。行と列が存在しますが、リレーショナルデータベース (RDB) と異なり、使用していない領域はストレージを消費しないスパース (低密度) な構造となっています。またダウンタイムなしでクラスタサイズを変更でき、スケーラビリティに優れています。 Bigtable のデータへは、 API 経由 でアクセスします。Go, Java, Python, PHP, Ruby, C# など各言語用のクライアントライブラリが用意されている他、Apache HBase (オープンソースの列指向・分散データベース) 互換の API も用意されています。 ユースケース Bigtable は大量データのリアルタイム処理で真価を発揮 します。 前述の Google のサービスで使われているほか、大量のログや IoT デバイスから送信されるデータを取り扱うケース等で採用されています。 クレジットカード利用における不正行為の検出 患者の容態の予測 フライトの高速検索システム 電力使用状況の可視化 Web 接客プラットフォーム ZOZOTOWN の推薦システム基盤 料金 概要 Bigtable では以下の料金が発生します。 コンピューティング料金 ストレージ料金 (バックアップ含む) 通信料金 料金表は 公式ページ を参照ください。 コンピューティング料金 コンピュート処理能力はノード (後述) 単位で最低1時間の課金が発生します。たとえば1台のノードが100分間稼働した場合は2時間分の料金が発生します。 また、実際にリクエストを処理していなくても課金が発生するため、リクエストが少ない時間帯はノード数を減らすことで料金を節約できます。 ストレージ料金 利用したストレージ (テーブルとバックアップ) 分だけ支払いが発生する従量課金です。設置するリージョンやディスク (SSD / HDD) に応じて単価が異なります。 Bigtable はコンパクションと呼ばれる自動圧縮処理を定期的に実行しており、課金はこの圧縮後のデータサイズに対して計算されます。 また複数のクラスタ (後述) を含むインスタンス (後述) の場合、クラスタごとにデータのコピーを保持するため、その分の課金が発生します。 ネットワーク料金 データの書き込み (Bigtable に入っていく方向) は無料です。 データの読み込み (Bigtable から出ていく方向) は同一リージョン内の通信は無料ですが、異なるリージョン間の通信やインターネットへ向けた通信には料金が掛かります。 計算例 参考までに料金の計算例を記載します (単価は2023年7月時点のもの)。 東京リージョンで 1 ヶ月を通して 1 ノードのコンピュートリソースを使用 平均 50 GB のデータを SSD ドライブに保存 東京リージョンへの 50GB のネットワーク下り通信 課金要素 数量 計 コンピューティング料金 1ノード * 30 days * 24h * $0.85 $612.00 ストレージ料金 (SSD) 50GB * $0.22 $11.00 ネットワーク料金 同一リージョン間通信のため無料 $0.00 - 合計 $623 データの操作 概要 一般的な RDB データの操作 (読取・書込等) には SQL を利用しますが、Bigtable は Web API を用います。 といっても直接 Web API への HTTP リクエストを行うことは稀です。実際には cbt という CLI ツールや、Python、Java、Go、PHP といった各言語に用意されたクライアントライブラリなど、Web API をラップしたツールを用いて操作するのが一般的です。 参考 : cbt CLI でインスタンスを作成してデータを書き込む 参考 : Bigtable client libraries クライアントライブラリ 例としてここでは Python のクライアントライブラリを用いて Bigtable にデータを登録するサンプルコードを紹介します。 greetings = [ "Hello World!" , "Hello Cloud Bigtable!" , "Hello Python!" ] rows = [] column = "greeting" .encode() for i, value in enumerate (greetings): row_key = "greeting{}" .format(i).encode() row = table.direct_row(row_key) row.set_cell( column_family_id, column, value, timestamp=datetime.datetime.utcnow() ) rows.append(row) table.mutate_rows(rows) row.set_cell(column_family_id, column, value, timestamp) にてテーブルにデータを追加しています。 このように Bigtable では行キー (row_key)、列名 (column)、値 (value) 等を設定してデータを登録します。 参考 : Python の Hello World SQL サポート Bigtable は原則的に、各プログラミング言語用のクライアントライブラリを用いてデータの読み書きを行うデータベースですが、2024年8月のアップデートで、SQL が利用可能になりました(2024年8月現在、Preview)。 GoogleSQL という、BigQuery などと共通の方言を持つ SQL を用いて、Bigtable にクエリを投入することができます。SQL はクライアントライブラリ経由で投入するか、Google Cloud コンソールの Bigtable Studio から投入することができます。 参考 : Introduction to SQL in Bigtable cbt CLI cbt CLI は Bigtable の操作を実行するためのツールです。Cloud Shell やローカル開発環境にインストールして Bigtable を操作することができます。 my-table という名前のテーブルは以下のコマンドで作成できます。 cbt createtable my-table my-table からデータを読み取るコマンドは以下になります。 cbt read my-table 参考 : クイックスタート: cbt CLI を使用してインスタンスを作成し、データを書き込む 他の Google Cloud サービス BigQuery (データウェアハウスサービス) から Bigtable を外部テーブルとして定義することで、Bigtable のデータを BigQuery にコピーしなくても、BigQuery から直接クエリを発行することができます。 参考 : Bigtable データにクエリを実行する また Dataflow (Apache Beam のマネージドサービス) から Bigtable に接続するためのコネクタが用意されており、Dataflow パイプラインの中から Bigtable のデータへアクセスすることが可能です。 参考 : Bigtable 用 Dataflow コネクタ コンポーネント 全体像 Bigtable のコンポーネント構成の全体像は、以下のとおりです。 全体像 参考 : インスタンス、クラスタ、ノード インスタンス インスタンス は Bigtable の最も基本的な管理単位です。 インスタンスは複数のクラスタとストレージを含み、テーブルもインスタンスに所属します。テーブルは後述のクラスタやノードに所属するのではなく、インスタンスに所属し、各クラスタにレプリケーションされます。 クラスタ クラスタ はノード (個々のサーバ) をグルーピングした概念です。一つのインスタンスに所属し、特定のゾーンに存在します。アプリケーションがインスタンスにリクエストを送信すると、いずれかのクラスタが処理します。 なお Bigtable ではコンピューティングとストレージが分離されているため、クラスタにはストレージが含まれません。 たとえば東京リージョン (内のとあるゾーン) と大阪リージョン (同) にクラスタを展開すると ①最寄りのクラスタでリクエストを処理するためレイテンシを低下できる ②東京リージョンで障害が発生しても大阪リージョンでサービスを継続できる といったことが可能になります。 ノード ノードはクラスタを構成するサーバのイメージです。ノードを増やすことでクラスタの処理能力を向上できます。 CPU 使用率やストレージ使用率に応じて自動的にノードを追加することも可能です。 自動スケーリング ストレージ ストレージはデータが保存される領域です。クラスタに所属します。 複数クラスタとレプリケーション 概要 Bigtable インスタンスの中には、複数のクラスタを配置できます。クラスタ間でストレージはレプリケーションされ、データの可用性と耐久性を向上させます。 Bigtable インスタンスは、Google Cloud リージョンのうち最大 8 つのリージョンにクラスタを配置できます。またリージョン内はゾーンで分かれていますが、一つのゾーンに配置できるクラスタは 1 つのみです。 あるリージョン内で複数のゾーンにクラスタを配置することもできますし、別々のリージョンにクラスタを配置することもできます。 参考 : レプリケーションについて レイテンシと整合性 クラスタ間のレプリケーションは非同期であり、レイテンシがあること、また結果整合性であることに注意が必要です。 レプリケーションのレイテンシがどのくらいあるかについては一概には言えませんが、通常は数秒〜数分の間であり、数時間に達することはありません。 ただし、書込後の読取に整合性を持たせることも可能です。後述のアプリプロファイルにて「単一クラスタのルーティング」を選択した場合のみ、単一行レベルでの強整合性を確保することが可能です。 参考 : 整合性モデル 複数クラスタのユースケース 可用性向上 複数クラスタを別々のゾーンに配置することで高い可用性を担保できます。自動フェイルオーバさせることも可能です。 参考 : 高可用性(HA)の作成 オンラインとバッチの分離 複数クラスタを用意することで、業務アプリケーションと分析目的のジョブのワークロードを分離することができます。 分離には、後述のアプリプロファイルを利用できます。 参考 : バッチ分析ワークロードを他のアプリケーションから分離する グローバルなレイテンシ短縮 世界中にアプリケーションの利用者がいる場合、利用者に近い地域のリージョンにクラスタを作成することで、読取レイテンシの低減に繋がります。 参考 : ユーザーの近くにデータを保存する バックアップ・リストア Bigtable では任意の時点の バックアップ を取得することができます。 Bigtable のバックアップはテーブルバックアップであり、指定したテーブルのスキーマとデータを保持します。Compute Engine のスナップショット等は異なり、インスタンス (あるいはクラスタやノード) をまるごとバックアップするものではありません。 バックアップからの復元時は、任意の既存インスタンスを選択してその中にテーブルをリストアできます。 バックアップからのリストアは、オペレーションミスやアプリケーションによりデータが破壊された場合などに加え、例えば本番環境テーブルからステージング環境テーブルを複製して作成する、などの用途にも使えます。 バックアップの復元 参考 : Bigtable のバックアップについて 内部構造 ストレージモデル まずは Bigtable に登録されたデータがどのように保管されるのか、ストレージモデルを見ていきます。ストレージモデルは後述のスキーマ設計に大きく関わってきます。 画像は 公式ドキュメント から引用 行 (Key) と列 (Value) で構成される Key-Value マップにデータを格納します。 各行は一意の行キーを持っています。 相互に関連する列を列ファミリーとしてグループ化できます。 行と列が交差する場所には複数のセルを含むことができます。 使用していない列はデータの保存領域を消費しません。 インフラ・アーキテクチャ Bigtable はデータを保管するストレージと、クエリを実行するコンピューティングリソース(ノード)が分離した構成になっています。 これにより大量データの保管と高速なクエリを実現しています。 実際にクエリが処理される流れを見ていきます。 アーキテクチャ クライアント リクエストは Bigtable クラスターに送信されます。 Bigtable クラスターは複数のノードで構成されており、リクエストが各ノードに割り当てられます。(図の場合、行キーが D から始まるクエリを処理する場合はノード 1 が割り当てられます) ノードはそれぞれ独立してリクエストを処理します。ノード追加によりパフォーマンスを向上できます。 実データは辞書順に並び替えられ、ストレージに保管されています。 なお、上図は各アルファベットから始まるデータが均等な場合のイメージです。実際には各ノードの負荷が均等になるようにノードとデータは関連付けられます。 スキーマ設計 スキーマ設計のポイント データベースのパフォーマンスを最大限発揮できるようなスキーマ設計は重要です。 従量課金のクラウドサービスでは過大な課金につながる可能性もあります。 アーキテクチャやストレージの仕組みのポイントをおさらいします。 Key-Value ストアであること テーブル結合は利用できません。 トランザクションは 1 つの行内でのみ完結します。複数行にまたがるトランザクションは利用できません。 行の特徴 各行キーは一意である必要があります。 行は、行キーのビッグエンディアン順(バイナリのアルファベット順に相当する)に並べ替えられます。 各テーブルのインデックス(行キー)は 1 つのみです。二次インデックスはありません。 列の特徴 列ファミリーは特定の順序では保存されません。 列は、列ファミリー別にグループ化され、列ファミリー内で辞書順に並べ替えられます。 読み取りと書き込みは(テーブルの行スペース全体に)均等に分散されるのが理想 Bigtable で使用していない列は空になり、保存領域を消費しない スキーマ設計のベストプラクティス 上記で整理した内容をスキーマ設計の観点に読み替えると、以下の 2 点が挙げられます。 データは結合不要な形で保管する。非正規化して1つのテーブルにまとめる 各ノードに均等にリクエストが割り当てられるようにする。時刻やシーケンス番号など、辞書順で並べた際に偏りのある情報は行キーの先頭では使用しない また、辞書順で偏らないためにハッシュ化した値を利用する方法もありそうですが、これはアンチパターンです。トラブルシューティングをする際、読解不可能な文字列では支障があるため、行キーには読解可能な文字列を使用します。 詳細はスキーマ設計の ベストプラクティス をご参照ください。 スキーマ設計例 時系列データのスキーマ設計例をみてみましょう。 気象バルーンが 1 分ごとに測定した圧力等のデータを Bigtable に保存することを想定します。 スキーマ設計に画一的な正解はなく、いくつかパターンがあります 。 自身のケースに置き換えてみて、適切なパターンを選択することが重要です。 測定するごとに行を追加するパターン 1分ごとに新しい行を登録するパターンです。 シンプルで開発が容易 なことが特徴です。 このパターンで書き込まれる例を示します。 行キーには1分ごとの日時を示す文字列が含まれます。 行キー 圧力 温度 湿度 標高 us-west2#3698#2023-06-05-1200 94558 9.6 61 612 us-west2#3698#2023-06-05-1201 94122 9.7 62 611 us-west2#3698#2023-06-05-1202 95992 9.5 58 602 us-west2#3698#2023-06-05-1203 96025 9.5 66 598 us-west2#3698#2023-06-05-1204 96021 9.6 63 624 測定するごとに列を追加するパターン 1分ごとに列を追加するパターンです。 1行にたとえば1週間分のデータを格納します。 保存容量を節約できる ことが特徴です。 このパターンで書き込まれる例として、3分後の pressure (圧力) のデータを示します。 行キーには n 週目の圧力を示す文字列が含まれます。 value は測定値に対する測定日時が登録されたデータ構造になります。 行キー 94558 94122 95992 us-west2#3698#pressure#week1 t2023-06-05-1200 t2023-06-05-1201 t2023-06-05-1202 毎回測定値が異なると列が増えてしまいますが、 同じような測定値が繰り返される場合は、複数のセルにまとまって情報が登録されるため保存容量が節約されます。 測定するごとにセルを追加するパターン 1分ごとにセルを追加するパターンです。 1行にたとえば1週間分のデータを格納します。 測定値の経時変化を扱える ことが特徴です。 3分後の圧力列と温度列は次のようになります。 行キーには n 週目を示す文字列が含まれます。 行キー 圧力 温度 asia-south2#3698#week1 94558(t2023-06-05-1200) 9.5(t2023-06-05-1200) 94122(t2023-06-05-1201) 9.4(t2023-06-05-1201) 95992(t2023-06-05-1202) 9.2(t2023-06-05-1202) 1 行の読み取りで 1 週間分のデータを読み取るため、経時変化を扱う場合に有用です。温度だけ必要であれば、温度の列だけ選択することも可能です。 詳細は 公式ドキュメント をご参照ください。 アプリプロファイル アプリプロファイル (app profiles) は、Bigtable がアプリケーションから受け取ったリクエストを処理する方法について定義した設定です。 アプリプロファイルは、複数のクラスタを使用するインスタンスで特に重要になります。トランザクションの整合性に関する設定や、クラスタへのルーティングの方法などを定義します。 例えば業務アプリケーションのトラフィックと、分析用のトラフィックに別々のアプリプロファイルを当てはめ、別々のクラスタへルーティングすることで、クラスタへの負荷を分散させることができます。 参考 : アプリ プロファイルについて アプリケーションプロファイル 以下はアプリ側の サンプルコード です。赤字箇所でアプリケーションプロファイルを指定しています。 from google.cloud import bigtable client = bigtable.Client(project=project_id) instance = client.instance(instance_id) table = bigtable.table.Table(table_id, instance, '[APP_PROFILE_ID]' ) パフォーマンスに関する注意点 概要 データの操作時も Bigtable の内部構造を考慮する必要があります。Bigtable のパフォーマンスを十分に発揮させるには以下の点が重要です。 辞書順でデータが格納されてノードが割り当てられることを考慮して、まとまった単位でデータを格納すること 類似の情報を一括して読み取り・書き込みし、 クエリの発行回数を抑える こと 多数のクエリを発行する場合は、1つのノードに偏らない こと 書き込み Bigtable では単一行を書き込む方法のほかに、複数行を同時に書き込むバッチ書き込みが利用できます。 隣接したデータを更新する場合は、バッチ書き込みを利用したほうがクエリの発行回数を抑えることができてパフォーマンスが良くなります。 参考 : Batch writes 読み取り Bigtable では行キーの辞書順でデータが保管されているため、 連続した複数行の読み取りは低レイテンシ です。 しかしランダムな複数行の読み取りはテーブル全体をスキャンすることになるため非効率です。 よく使用するクエリが低レイテンシで応答できるよう、 同時に読み取ることの多いデータが近くなるようなスキーマ設計 を意識すると良いでしょう。 また、 読み取る列を絞り込むことでパフォーマンスを改善する ことができます。 参考 : Reads and performance モニタリング リソース使用状況の確認 Bigtable のコンソール画面や Cloud Monitoring のコンソール画面で CPU 使用率やディスクの使用量などを モニタリング することができます。 CPU 使用率やディスク使用量が増加傾向にあれば、必要に応じてノードを追加して負荷を分散させましょう。 Key Visualizer Key Visualizer は Bigtable の使用状況の分析に役立つツールです。 テーブルの行全体がバランスよくアクセスされているかどうか等を確認することができます。 以下の画像は Key Visualizer スキャンの画面です。 横軸が時間で縦軸が行キーを表しており、色が明るいほど読み込みや書き込みなどの処理が実行されていることを示します。たとえば特定の行キーにのみ明るい色がついている場合、そこに負荷が集中していることが判ります。 均等分布や順次読み取り/書き込みの場合等、いくつかのパターンが こちら で紹介されています。 画像は 公式ドキュメント から引用 片岩 裕貴 (記事一覧) クラウドソリューション部 クラウドディベロッパー課 2022年5月にG-genにジョインした和歌山県在住のエンジニア。興味分野はAI/ML。2024年にGoogle Cloud認定資格全冠達成。最近は子供と鈴鹿サーキットや名古屋のレゴランドに行ってきました。
当記事では、Google Cloud (旧称 GCP) の Compute Engine VM から Cloud Storage バケットを操作する時に起きる権限エラーについて、実際のエラー内容からサービスアカウントの IAM 権限以外に疑うこと、その対処法について紹介します。 前提知識 事象 原因 エラー文の違い 対処 アクセススコープの変更 手動で作成したサービスアカウントをアタッチ ユーザーアカウントの使用 前提知識 Compute Engine (以下 GCE)は、デフォルトでは PROJECT_NUMBER-compute@developer.gserviceaccount.com のサービスアカウントが設定されます。但し、このサービスアカウントにはプロジェクトレベルで 編集者 ( roles/editor )ロールが付与されており、広範囲な権限を持っているため実運用での利用は好ましくありません。 参考: Compute Engine のデフォルトのサービス アカウント 事象 GCE インスタンスでデフォルトのサービスアカウントをアタッチした状態で Cloud Storage(以下 GCS)を操作すると、バケットやオブジェクトの閲覧はできても、アップロードや削除ができませんでした。 # バケットの表示:可 fujioka@instance:~$ gcloud storage ls gs://test-bucket/ fujioka@instance:~$ # ファイルのアップロード:不可 fujioka@instance:~$ gcloud storage cp test .txt gs://test-bucket/ Copying file:// test .txt to gs://test-bucket/ test .txt ERROR: User [ 012345-compute@developer.gserviceaccount.com ] does not have permission to access b instance [ test-bucket ] ( or it may not exist ) : Access denied. Completed files 1 / 1 | 0B fujioka@instance:~$ デフォルトのサービスアカウントには編集者ロールが付与されています。バケットから確認しても、編集者ロールが継承され、 ストレージ管理者 ( roles/storage.admin )、が設定されており、サービスアカウントの IAM 権限としては問題ありません。 バケットの権限 この時、Cloud Logging にはエラーログは出力されていませんでした。 参考: gcloud storage 参考: Cloud Storage に適用される IAM のロール 原因 今回は、サービスアカウントの IAM は編集者ロールのため GCS バケットにオブジェクトのアップロードが出来るはずですが、アクセススコープが読み取りしか許可をしていないため、制約の厳しいアクセススコープが優先され生じた権限エラーでした。 詳しく見ていきます。 GCE インスタンスから Google Cloud APIs へのアクセス制御方法には以下の 2 つがあります。 サービスアカウントの IAM アクセススコープ サービスアカウントとアクセススコープについての詳細は以下の記事をご参照ください。 blog.g-gen.co.jp blog.g-gen.co.jp 全てデフォルトでインスタンスを作成すると、 API と ID の管理 で、サービスアカウントと Cloud API アクセススコープは以下のように設定されます。 インスタンスの詳細画面 Cloud API アクセススコープが デフォルトのアクセス権を許可 になっている時は、GCS に対して読み取り専用のアクセス権( https://www.googleapis.com/auth/devstorage.read_only )が与えられます。この時、 サービスアカウントの IAM とアクセススコープのうち、制約が厳しい方が優先されます 。今回のエラー原因は、このアクセススコープが デフォルトのアクセス権を許可 の設定だったためです。 GCS 以外にもアクセススコープは以下のようなサービスの制御が可能です。 API ごとのアクセススコープ gcloud compute instances create で GCE インスタンスを作成した場合もデフォルトではコンソールと同様のアクセススコープ設定となるため注意が必要です。 参考: デフォルトのスコープ 参考: gcloud compute instances create エラー文の違い アクセススコープ起因とサービスアカウントの IAM 起因のそれぞれの権限エラーでは、エラーの表示に以下のような違いがあります。 # アクセススコープ起因 fujioka@instance:~$ gcloud storage cp test .txt gs://test-bucket/ Copying file:// test .txt to gs://test-bucket/ test .txt ERROR: User [ 012345-compute@developer.gserviceaccount.com ] does not have permission to access b instance [ test-bucket ] ( or it may not exist ) : Access denied. Completed files 1 / 1 | 0B fujioka@instance:~$ # サービスアカウントの IAM 起因 fujioka@instance:~$ gcloud storage cp test .txt gs://test-bucket/ Copying file:// test .txt to gs://test-bucket/ test .txt ⠹ERROR: User [ test@ 012345 .iam.gserviceaccount.com ] does not have permission to access b instance [ test-bucket ] ( or it may not exist ) : test-59@ 012345 .iam.gserviceaccount.com does not have storage.objects.create access to the Google Cloud Storage object. Permission ' storage.objects.create ' denied on resource ( or it may not exist ) . Completed files 1 / 1 | 0B fujioka@instance:~$ サービスアカウントの IAM 権限が不足している時には、具体的に不足している権限(ここでは storage.objects.create )がエラー文に含まれています。 対処 アクセススコープの変更 アクセススコープは、以下の 3 種類から選べます。 デフォルトのアクセス権を許可 すべての Cloud API に完全アクセス権を許可 API ごとにアクセス権を設定 アクセススコープの種類 アクセススコープは、 すべての Cloud API に完全アクセス権を許可 ( https://www.googleapis.com/auth/cloud-platform ) にし、アクセス制御はサービスアカウントの IAM で行うことが推奨されています 。 アクセススコープで GCE インスタンスのアクセス制御をすることはレガシーな方法です。 今回は、デフォルトのサービスアカウントのままアクセススコープを すべての Cloud API に完全アクセス権を許可 に変更することで権限エラーは解消されました。 参考: スコープのベスト プラクティス 手動で作成したサービスアカウントをアタッチ 実運用では前述の通り、デフォルトのサービスアカウントの利用は推奨されていません。 そのため、ベストプラクティスはサービスアカウントを作成し、最小権限の原則に従い適切な IAM 権限を付与したサービスアカウントを GCE インスタンスにアタッチすることです。 この時、手動で作成したサービスアカウントに変更すると、以下のようにアクセススコープが選択できなくなります。 アクセススコープが選択不可になる 参考: ベスト プラクティス 参考: IAM を安全に使用する ユーザーアカウントの使用 方法論としてユーザーアカウントを使うことで今回のエラーは回避できますが、「アクセススコープの変更」と「手動で作成したサービスアカウントをアタッチ」を行う方が好ましいです。ここでは参考までに記載します。 デフォルトでは、GCE インスタンス上で gcloud CLI を使う際、 アプリケーションのデフォルト認証情報 (ADC)を使ってサービスアカウントの認証情報とアクセススコープに従いアクセス制御を行います。 GCE インスタンスの設定を確認すると、以下のようにデフォルトのサービスアカウントが設定されていることがわかります。この状態では、アクセススコープの変更をしていないため、 gcloud storage cp~ でエラーとなります。 # アカウントの確認 fujioka@instance:~$ gcloud config list [ core ] account = 012345-compute@developer.gserviceaccount.com disable_usage_reporting = True project = 012345 Your active configuration is: [ default ] fujioka@instance:~$ # ファイルのアップロード:不可 fujioka@instance:~$ gcloud storage cp test .txt gs://test-bucket/ Copying file:// test .txt to gs://test-bucket/ test .txt ERROR: User [ 012345-compute@developer.gserviceaccount.com ] does not have permission to access b instance [ test-bucket ] ( or it may not exist ) : Access denied. Completed files 1 / 1 | 0B fujioka@instance:~$ ここで、適切な権限を持ったユーザーアカウントに切り替えると、gcloud CLI の実行はユーザーアカウントになるため、 gcloud storage cp~ が問題なく実行できます。 # ユーザーアカウントを使う fujioka@instance:~$ gcloud auth login You are running on a Google Compute Engine virtual machine. It is recommended that you use service accounts for authentication. ~ You are now logged in as [ fujioka@g-gen.co.jp ] . Your current project is [ 012345 ] . You can change this setting by running: $ gcloud config set project PROJECT_ID fujioka@instance:~$ # アカウントの確認 fujioka@instance:~$ gcloud config list [ core ] account = fujioka@g-gen.co.jp disable_usage_reporting = True project = 012345 Your active configuration is: [ default ] fujioka@instance:~$ # ファイルのアップロード:可 fujioka@instance:~$ gcloud storage cp test .txt gs://test-bucket/ Copying file:// test .txt to gs://test-bucket/ test .txt Completed files 1 / 1 | 0B fujioka@instance:~$ 参考: gcloud CLI を承認する G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
今回は Cloud Run jobs を用いて、FTP サーバから Cloud Storage にファイル転送する仕組みを作成していきます。 概要 事前準備 開発環境の準備 ディレクトリ構成 Dockerfile main.py requirements.txt 使用するリソースの作成 FTP サーバの初期設定 動作確認 Cloud Run jobs の実行 リソースの削除 本番運用に向けての考慮事項 接続方式 IP アドレス プロトコル サービスアカウントの権限 概要 データ分析パイプラインなどで、データソースが FTP サーバ上に存在する時、FTP サーバからクラウド上のデータレイクにデータ転送が必要なケースもあるかと思います。 今回は、先日 (2023 年 4 月) GA した Cloud Run jobs を用いて、FTP サーバから Cloud Storage にファイル転送する仕組みを作成していきたいと思います。 今回作成する構成図 Cloud Run jobs は Cloud Scheduler や Cloud Workflows と連携することでスケジュール実行することも可能ですが、今回は検証のため gcloud コマンドを用いて手動実行していきます。 Cloud Run jobs の詳細については、以下の記事をご参照下さい。 blog.g-gen.co.jp 事前準備 開発環境の準備 ディレクトリ構成 開発環境には Cloud Shell を使用します。Cloud Shell を起動したら、以下のディレクトリ構成で各ファイルを作成して下さい。 get_file_from_ftp_server |-- Dockerfile |-- main.py `-- requirements.txt Dockerfile # Pythonイメージを取得 FROM python:3. 10 # ローカルコードをコンテナイメージにコピー ENV APP_HOME /app WORKDIR $APP_HOME COPY . ./ # 依存関係のインストール RUN pip install --no-cache-dir -r requirements.txt # コンテナの起動時の実行コマンド CMD [" /usr/local/bin/python3 " , " main.py "] main.py main.py には以下を記述して下さい。 使用したライブラリについて、FTP 接続には ftplib を、Cloud Storage バケットへのアップロードには、 Python Client for Google Cloud Storage を使用しました。 from ftplib import FTP import os from google.cloud import storage FTP_SERVER_IP = os.environ.get( "FTP_SERVER_IP" ) FTP_USER = os.environ.get( "FTP_USER" ) FTP_PASSWD = os.environ.get( "FTP_PASSWD" ) FILE_NAME = os.environ.get( "FILE_NAME" ) BUCKET_NAME = os.environ.get( "BUCKET_NAME" ) if __name__ == "__main__" : try : # FTP サーバへ接続 ftp = FTP(host=FTP_SERVER_IP, user=FTP_USER, passwd=FTP_PASSWD) # パッシブモードを有効 ftp.set_pasv( True ) except Exception as e: print ( "An error occurred connecting to the ftp server:" , str (e)) raise else : try : # FTP サーバからファイル取得 filename = FILE_NAME ftp.retrbinary( "RETR " + filename, open (filename, "wb" ).write) ftp.quit() except Exception as e: print ( "An error occurred when retrieving files from the ftp server:" , str (e)) raise else : try : # Cloud Storage へアップロード storage_client = storage.Client() bucket = storage_client.bucket(BUCKET_NAME) blob = bucket.blob(filename) blob.upload_from_filename(filename) except Exception as e: print ( "An error occurred while uploading a file to Cloud Storage:" , str (e)) raise requirements.txt google-cloud-storage == 2 . 9 . 0 使用するリソースの作成 Cloud Shell で get_file_from_ftp_server のディレクトリ階層に移動したら、以下コマンドを順次実行します。 # 環境変数の設定 export PROJECT_ID = { プロジェクト ID を入力 } export PROJECT_NAME = { プロジェクト名を入力 } export BILLING_ACCOUNT_ID = { 請求先アカウント ID を入力 } export BUCKET_NAME = { 取得するファイルの格納先バケット名を入力 } export FTP_USER =ftpuser export FTP_SERVER_PW = 1234 export FILE_NAME =sample.txt # プロジェクト作成 gcloud projects create ${PROJECT_ID} --name = ${PROJECT_NAME} # プロジェクト設定の変更 gcloud config set project ${PROJECT_ID} # 請求先アカウントの紐づけ gcloud beta billing projects link ${PROJECT_ID} \ --billing-account = ${BILLING_ACCOUNT_ID} # API の有効化 gcloud services enable compute.googleapis.com \ secretmanager.googleapis.com \ run.googleapis.com \ artifactregistry.googleapis.com \ cloudbuild.googleapis.com # FW の作成 gcloud compute firewall-rules create ftp-rule \ --allow tcp:20-21,tcp:40000-45000 # VM の作成 gcloud compute instances create " ftp-server " \ --zone =" us-central1-a " \ --machine-type =" e2-micro " \ --image-family =" debian-11 " \ --image-project =" debian-cloud " \ --boot-disk-size =" 10 " \ --boot-disk-type =" pd-standard " # バケットの作成 gcloud storage buckets create gs:// ${BUCKET_NAME} #シークレットの作成 gcloud secrets create my_password --replication-policy =" automatic " # シークレットにバージョンの追加 echo -n ${FTP_SERVER_PW} | gcloud secrets versions add my_password --data-file = - # プロジェクト番号を環境変数に追加 export PROJECT_NO = $( gcloud projects list --filter = ${PROJECT_ID} --format =" value(projectNumber) " ) # シークレットへ読み取り権限をCompute Engineのデフォルトサービスアカウントに付与 gcloud secrets add-iam-policy-binding my_password \ --member =" serviceAccount: ${PROJECT_NO} -compute@developer.gserviceaccount.com " \ --role =" roles/secretmanager.secretAccessor " # アーティファクトリポジトリの作成 gcloud artifacts repositories create docker-repo-ftp \ --repository-format =" docker " \ --location =" us-central1 " \ --description =" Docker repository " # Dockerイメージのビルド gcloud builds submit --region =" us-central1 " \ --tag =" us-central1-docker.pkg.dev/ ${PROJECT_ID} /docker-repo-ftp/jobs:latest " # VM の外部 IP を環境変数に追加 export FTP_SERVER_IP = $( gcloud compute instances describe ftp-server \ --zone =" us-central1-a " \ --format =" get(networkInterfaces[0].accessConfigs[0].natIP) " ) # Cloud Run jobsの作成 gcloud run jobs create ftp-job \ --image =" us-central1-docker.pkg.dev/ ${PROJECT_ID} /docker-repo-ftp/jobs:latest " \ --region =" us-central1 " \ --set-env-vars =" FTP_SERVER_IP= ${FTP_SERVER_IP} " \ --set-env-vars =" FTP_USER= ${FTP_USER} " \ --set-env-vars =" FILE_NAME= ${FILE_NAME} " \ --set-env-vars =" BUCKET_NAME= ${BUCKET_NAME} " \ --set-secrets =" FTP_PASSWD=my_password:latest " FTP サーバの初期設定 先程作成した ftp-server VM に SSH でログインし、FTP サーバの初期設定を行っていきます。 gcloud compute ssh ftp-server --zone =" us-central1-a " --project = ${PROJECT_ID} ターミナルに ユーザー名@ホスト名:~$ が表示されたらログイン成功です。 はじめに、OS のバージョンを確認します。 # OS のバージョン確認 matayuuu@ftp-server:~$ lsb_release No LSB modules are available. Distributor ID: Debian Description: Debian GNU/Linux 11 ( bullseye ) Release: 11 Codename: bullseye 次に、以下コマンドを実行します。 # パッケージのアップデート sudo apt-get update # FTPサーバーをインストール sudo apt-get install vsftpd # VM の外部 IP を確認 (後ほど使用するので出力された IP アドレスをメモしておく) curl -s ifconfig.me # vsftpd 設定ファイルを編集 sudo vi /etc/vsftpd.conf vsftpd 設定ファイルの中身を以下のように加筆修正します。 既存の設定を修正 listen=NO → listen=YES listen_ipc6=YES → listen_ipc6=NO 新規の設定を追加 (最後の行に追記) pasv_enable=YES pasv_min_port=40000 pasv_max_port=45000 pasv_address=${先程メモしたVMの外部IP} 1. 既存の設定を修正 では、FTP サーバがスタンドアロンモードで動作するよう、また IPv4 アドレスのみを許可するように設定しています。 2. 新規の設定を追加 では、FTP サーバがパッシブモードで動作するよう、また パッシブモード時に使用するポート範囲と FTP 接続を確立する IP アドレスを設定しています。 続けて以下のコマンドを実行します。 # vsftpdを再起動 sudo systemctl restart vsftpd # フォルダの作成 sudo mkdir /home/ftpuser # ファイルを作成 echo " Hello, FTP! " | sudo tee /home/ftpuser/sample.txt # ユーザーの作成(検証のためパスワードは「1234」と簡潔なものとし、その他はデフォルト設定) sudo adduser ftpuser # ユーザーをFTPグループに追加 sudo usermod -aG ftp ftpuser # ftpuserディレクトリの所有者をftpuser、グループをftpに変更 sudo chown ftpuser:ftp /home/ftpuser # ftpuserディレクトリの書き込み権限を全削除 sudo chmod a-w /home/ftpuser # ログアウト exit 動作確認 Cloud Run jobs の実行 以下のコマンドで Cloud Run jobs を手動実行します。 gcloud run jobs execute ftp-job \ --region =" us-central1 " Cloud Run jobs のコンソール画面からジョブの実行結果を確認できます。 ジョブの詳細画面 Cloud Storage に FTP サーバから取得した sample.txt も保存されていることが確認できました。 Cloud Storage の画面 sample.txt リソースの削除 以下のコマンドを実行し、検証で作成したプロジェクトを削除します。 gcloud projects delete ${PROJECT_ID} 本番運用に向けての考慮事項 接続方式 IP アドレス 今回は FTP サーバに外部 IP を付与しすべてのソース元 IP アドレスを許可しましたが、本番運用では ①内部 IP アドレスのみを許可 、もしくは ②許可した外部 IP アドレスのみを許可 する構成が想定されます。 必要に応じ、 サーバレス VPC アクセス や Cloud NAT を用いて Cloud Run jobs から FTP サーバへの通信を制御していく必要があります。 それぞれの構成図 参考 : Connect to a VPC network 参考 : Static outbound IP address プロトコル インターネット経由でファイル転送を行う際は、セキュリティの観点から FTP プロトコルより SFTP プロトコルを採用することが多いです。その際は、SFTP サーバの SSH 認証キーを Secret Manager に保存して管理する等の必要があります。 サービスアカウントの権限 今回は Cloud Run に Compute Engine のデフォルトサービスアカウントをアタッチしましたが、必要最低限のロールを付与したサービスアカウントを作成することが推奨されます。 G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
当記事では、Google Cloud (旧称 GCP) のリソースを Terraform で作成する際に生じる "reason": "SERVICE_DISABLED" エラーへの対処として Terraform の time_sleep を紹介します。 前提知識 エラー文 原因 対処法 前提知識 Google Cloud は Google Cloud APIs と呼ばれる Web API 群から成り立っています。そのため、Google Cloud ではサービス利用時に対象の API を有効化する必要があります。 例えば、VPC や GCE のリソースを作成するには compute.googleapis.com の API を有効化します。この API 有効化のステップは Amazon Web Services (AWS) にはなく、Google Cloud 特有のものです。 Google Cloud APIs についての詳細は以下の記事をご参照ください。 blog.g-gen.co.jp Terraform で API を有効にする際は google_project_service を使います。Terraform の基本操作については以下の記事をご参照ください。 blog.g-gen.co.jp エラー文 例として、Terraform で VPC リソースを作成しようとすると以下のエラーが発生する場合があります。 このエラーメッセージは、「Compute Engine API が有効化されていない」旨を示しています。しかし、実際には Terraform のコード内で google_project_service リソースが定義されており、これによって Compute Engine API が有効化されているはずです(Terraform 実行後にコンソールから確認したところ、問題なく有効になっていました)。 fujioka @ cloudshell :~/ terraform ( xxx )$ terraform apply ~ │ Error : Error creating Network : googleapi : Error 403: Compute Engine API has not been used in project xxxx before or it is disabled . Enable it by visiting https : //console.developers.google.com/apis/api/compute.googleapis.com/overview?project=xxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. │ Details : │ [ │ { │ " @type ": " type.googleapis.com/google.rpc.Help ", │ " links ": [ │ { │ " description ": " Google developers console API activation ", │ " url ": " https : //console.developers.google.com/apis/api/compute.googleapis.com/overview?project=xxxx" │ } │ ] │ } , │ { │ " @type ": " type.googleapis.com/google.rpc.ErrorInfo ", │ " domain ": " googleapis.com ", │ " metadatas ": { │ " consumer ": " projects/xxxx ", │ " service ": " compute . googleapis . com " │ } , │ " reason ": "SERVICE_DISABLED" │ } │ ] │ , accessNotConfigured │ ~ fujioka @ cloudshell :~/ terraform ( xxxx )$ 原因 API の有効化が完了するまでは時間がかかります。これは、 コンソールから有効化する 場合も同様です。 そのため、Terraform の depends_on で実行順序を制御しても API の有効化完了が間に合わないと上記のようなエラーとなります。 この場合、API の有効化が間に合っていないだけのため、時間を置いてから Terraform を再実行することでエラーは解消されます。しかし、根本的な解決方法は、API の有効化が完了するまである程度待機してから、後続のアクションを実行することです。 対処法 Terraform の time_sleep を使うことで、後続の実行までにスリープ時間を作ることができます。 create_duration の部分でスリープ時間を調整できます。この場合、API の有効化( google_project_service )の後にスリープ時間を作るよう depends_on で実行順序を制御します。 resource "google_project_service" "enabled_apis" { service = "compute.googleapis.com" } resource "time_sleep" "wait_30_seconds" { depends_on = [ google_project_service.enabled_apis ] create_duration = "30s" } ここでは sleep を入れることで解決しましたが、API を有効化後に無効にするケースは少ないことや、有効化する API が多ければその分 tfstate ファイルが肥大化してしまうことを考えると、API の有効化は Terraform でなくシェルスクリプト等で管理する選択肢も検討できます。 参考: time_sleep (Resource) G-gen 編集部 (記事一覧) 株式会社G-genは、サーバーワークスグループとして「クラウドで、世界を、もっと、はたらきやすく」をビジョンに掲げ、クラウドの導入から最適化までを支援している Google Cloud 専業のクラウドインテグレーターです。
当記事は みずほリサーチ&テクノロジーズ × G-gen エンジニアコラボレーション企画 で執筆されたものです。 みずほリサーチ&テクノロジーズ株式会社の浅香です。 今回は Google Cloud 上に Terraform Enterprise を実際に構築する機会を頂いたので、その構築内容/手順をご紹介させていただきます。 当ブログは G-gen × みずほRT によるコラボ記事です はじめに 構築環境 前提 airgap モジュールと TFE ライセンスファイルの調達 Google Cloud での構築 ドメインと DNS 名 インストール方法 証明書 構築手順 事前準備 構築作業 1. Root ユーザーにスイッチする 2. Docker を起動し、サービスとして登録する (状態確認) 3. TFE の設定ファイル (json) を作成する 4. replicated の設定内容 5. カレントディレクトリの移動 & installer bootstrapper の解凍 6. インストール実行 7. 起動チェック 設定作業 初期管理ユーザの作成 1. IACT(Initial Admin Creation Token) の発行 (shell) 2. Admin User の作成 (API) Organization (組織) の作成 1. 組織の作成 (API) Bundle ファイルを作成 1. Cloud Shell の起動 2. Github リポジトリのクローン 3. カレントディレクトリの移動 4. Go 言語でビルド 5. 確認 6. Bundle 用の HCL ファイルを作成 7. Bundle ファイルの作成 Bundle ファイルの登録 1. Bundle ファイルの配置 2. URL および checksum 結果の取得 3. Bundle ファイルの登録 (新規登録) 終わりに はじめに Terraform Enterprise (TFE)は、インフラストラクチャのプロビジョニングと管理を自動化するための強力なツールです。 TFEを使用することで、チーム全体でのインフラストラクチャの共有、セキュリティの向上、作業の追跡と可視化を容易に行うことができます。 また、金融システムなど高いセキュリティを求められる場合、拠点とパブリッククラウドを専用線で結びインターネット接続を制限するケースがあると思います。 今回、構築環境として利用した Google Cloud もそのような閉域環境としています。 この記事では、閉域にした Google Cloud 上にて利用する TFE の構築手順と設定について詳しく解説します。 構築環境 今回は Google Cloud の Compute Engine (以下、GCE) 上に構築することにします。 TFE環境 当該環境は閉域網であり、インタネット経由での各種アクセスはできないものとします。 (インターネット経由で取得する必要があるものは、別環境で取得して持ち込んでいるものとします。) 前提 この記事では以下の前提や制約条件があるものとします。 airgap モジュールと TFE ライセンスファイルの調達 TFE の利用に際して必要となる airgap モジュールとライセンスファイルが必要になります。 必要な際は こちら より HashiCorp 社へお問い合わせ下さい。 なお、installer bootstrapper は こちら からダウンロード可能です。 Google Cloud での構築 Terraform Enterprise GCP Reference Architecture では、可用性などを高めるために Cloud SQL や Cloud Storage の利用を推奨されていますが、今回は便宜的に GCE インスタンスのみで構築することにします。 インスタンスサイズやタイプについては、同ページに記載されている Infrastructure Requirements に準拠するものとします。 今回利用する OS は Supported Operating Systems にもある RHEL8 とします。 構築にあたって必要となる Docker Engine は事前にインストールしているものとします。 SMTP の設定は割愛します。(メール発信はできません。) ドメインと DNS 名 今回はカスタムドメイン (xxx.internal) を利用し、 tfe.xxx.internal| を使用します。 インストール方法 インストール方法としては大きく分けて Interactive Install と Automated Install の 2 種類がありますが、今回は後者の Automated Install にてインストールします。 なお、前者の Interactive Install は、こちらはブログ「 Deploying Terraform Enterprise in Air Gapped Environments 」が参考になります。 証明書 今回は自己証明書にて構築します。 構築手順 事前準備 SSHを使用してインスタンスに接続可能 以下のディレクトリを用意 用途 今回のディレクトリ TFE インストールに必要なモジュール配置先 /opt/tfe-module TFE 利用時に必要なデータ格納先 /opt/terraform-enterprise 自己署名証明書の発行 以下のファイルをサーバ内に格納 用途 今回の格納先 ライセンスファイル /opt/tfe-module/License.rli airgap モジュール /opt/tfe-module/tfe.airgap installer bootstrapper /opt/tfe-module/latest.tar.gz サーバ秘密鍵 /opt/tfe-module/server.crt 自己署名証明書 /opt/tfe-module/server.key 構築作業 1. Root ユーザーにスイッチする sudo su - 2. Docker を起動し、サービスとして登録する (状態確認) systemctl enable --now docker sudo systemctl status docker 3. TFE の設定ファイル (json) を作成する cat <<EOF > /opt/tfe-module/settings.json { "hostname": { "value": "tfe.xxx.internal" }, "capacity_concurrency": { "value": "10" }, "capacity_cpus": { "value": "0" }, "capacity_memory": { "value": "512" }, "enc_password": { "value": "<暗号化パスワード>" }, "log_forwarding_enabled": { "value": "1" }, "log_forwarding_config": { "value": "[OUTPUT]\n Name syslog\n Match *\n Host localhost\n Port 5140\n syslog_message_key message\n syslog_severity_key PRIORITY\n syslog_hostname_key _HOSTNAME\n syslog_appname_key SYSLOG_IDENTIFIER\n syslog_procid_key _PID" }, "production_type": { "value": "disk" }, "disk_path": { "value": "/opt/terraform-enterprise" } } EOF 設定可能な項目は こちら をご参照ください。 なお、今回は以下の通りとしています。 設定項目 設定値 hostname.value ドメイン名 tfe.xxx.internal capacity_concurrency.value 同時実行数 10 capacity_cpus.value CPU コアの最大数 0(無制限) capacity_memory.value メモリの最大量(メガバイト単位) 512 enc_password.value 内部管理型 Vault 用のパスワード <暗号化パスワード> log_forwarding_enabled.value ログ転送を有効化 1(有効化) log_forwarding_config.value ログ転送設定 後述 production_type.value ストレージ (blob以外) disk disk_path.value ストレージ利用先のディレクトリ /opt/terraform-enterprise ログ転送設定については、改行コードを \n に変換して設定する必要があるため上述の記載としていますが、改行すると以下の通りとなります。 今回はローカルホストの syslog を対象とした設定としていますが、その他の転送先については こちら をご参照ください。 [OUTPUT] Name syslog Match * Host localhost Port 5140 syslog_message_key message syslog_severity_key PRIORITY syslog_hostname_key _HOSTNAME syslog_appname_key SYSLOG_IDENTIFIER syslog_procid_key _PID 4. replicated の設定内容 cat <<EOF > /etc/replicated.conf { "DaemonAuthenticationType": "password", "DaemonAuthenticationPassword": "<ログインパスワード>", "BypassPreflightChecks": true, "TlsBootstrapType": "key-cert", "TlsBootstrapCert": "$(cat /opt/tfe-module/server.crt | sed -z 's/\n/\\n/g'| rev | cut -c 3- | rev)", "TlsBootstrapKey": "$(cat /opt/tfe-module/server.key | sed -z 's/\n/\\n/g'| rev | cut -c 3- | rev)", "ImportSettingsFrom": "/opt/tfe-module/settings.json", "LicenseFileLocation": "/opt/tfe-module/License.rli", "LicenseBootstrapAirgapPackagePath":"/opt/tfe-module/tfe.airgap" } 設定可能な項目は こちら をご参照ください。なお、今回は以下の通りとしています。 設定項目 設定値 DaemonAuthenticationType 認証方式 password DaemonAuthenticationPassword ログインパスワード <ログインパスワード> BypassPreflightChecks プリフライトチェックなし起動 true TlsBootstrapType TLS 証明書の種類 key-cert TlsBootstrapCert サーバ秘密鍵 $(cat /opt/tfe-module/server.crt | sed -z 's/\n/\\n/g'| rev | cut -c 3- | rev) TlsBootstrapKey 自己署名証明書 $(cat /opt/tfe-module/server.key | sed -z 's/\n/\\n/g'| rev | cut -c 3- | rev) ImportSettingsFrom 設定ファイルパス /opt/tfe-module/settings.json LicenseFileLocation ライセンスファイルパス /opt/tfe-module/License.rli LicenseBootstrapAirgapPackagePath パッケージファイルパス /opt/tfe-module/tfe.airgap サーバ秘密鍵と自己署名証明書については、ログ転送設定と同様に、改行コードを \n に変換して設定する必要があります。 今回は上述のコマンドにて実施しています。 5. カレントディレクトリの移動 & installer bootstrapper の解凍 cd /opt/tfe-module && tar xzf /opt/tfe-module/latest.tar.gz --remove-files 6. インストール実行 ./install.sh \ airgap \ no-proxy \ private-address=$PRIVATE_IP 7. 起動チェック while ! curl -ksfS --connect-timeout 5 https://tfe.xxx.internal/_health_check; do sleep 5 done インストールが終了したら、/_health_check エンドポイントが 200 を返します。 これによりアプリケーションが完全に起動したことを確認できます。 設定作業 初期管理ユーザの作成 構築作業が完了したら、TFE 利用に向けて各種設定を実施していきます。 最初に、製品の使用を開始するために初期管理ユーザを作成する必要があります。 いくつか方法がありますが、今回もコマンドにて実施していきます。 1. IACT(Initial Admin Creation Token) の発行 ( shell ) initial_token=$(replicated admin --tty=0 retrieve-iact | tr -d '\r') 2. Admin User の作成 ( API ) cat <<EOF >payload.json { "username": "tfe-admin-user", "email": "<メールアドレス>", "password": "<ユーザーパスワード>" } EOF curl \ --header "Content-Type: application/json" \ --request POST \ --data @payload.json \ https://tfe.xxx.internal/admin/initial-admin-user?token=$initial_token Response { "status": "created", "token": "aabbccdd.v1.atlas.ddeeffgghhiijjkkllmmnnooppqqrrssttuuvvxxyyzz" } Organization (組織) の作成 次に、Terraform Enterprise 上の Organization (組織) を作成します。設定に際しては、上述の Admin User 作成手順の Reeponse の token を使用します。 今回は 1 組織のみとしていますが、複数組織を作成する場合は、必要な個所を修正したうえで以下の手順を繰り返してください。 1. 組織の作成 ( API ) cat <<EOF >payload.json { "data": { "type": "organizations", "attributes": { "name": "test-org", "email": "sample.adrdess@xxx.com" } } } EOF curl \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/vnd.api+json" \ --request POST \ --data @payload.json \ http://tfe.xxx.internal/api/v2/organizations Bundle ファイルを作成 ここまでで組織の作成まで終わりましたが、TFE で利用可能な Terraform の Bundle ファイルが初期設定では https://releases.hashicorp.com/terraform/<version>/terraform_<version>_linux_amd64.zip" 等になっているため、閉域網で実行するとクライアント側 ( terraform plan や terraform apply を実行する側) でエラーになってしまいます。 そこで、Terrafom-bundle と API を使用して、閉域網内で実行可能なように設定していきます。 まずは Bundle ファイルを作成します。 ただし、Bundle ファイルの作成にあたってはインターネット接続環境が必要になります。今回は Cloud Shell で実施します。 1. Cloud Shell の起動 2. Github リポジトリのクローン git clone --single-branch --branch=v0.15 --depth=1 https://github.com/hashicorp/terraform.git 3. カレントディレクトリの移動 cd terraform 4. Go 言語でビルド go build -o ../terraform-bundle ./tools/terraform-bundle 5. 確認 ~/terraform-bundle -help 6. Bundle 用の HCL ファイルを作成 cat <<EOF>~/tfe_bundle.tf terraform { version = "<作成したいTerrafomのバージョン>" } providers { <Provider 名> = { source = "<ソース>" versions = [<バージョン>] } google = { source = "hashicorp/google" versions = ["~> 4"] } } EOF 7. Bundle ファイルの作成 ~/terraform-bundle package -os=linux -arch=amd64 ~/tfe_bundle.tf Response ~省略~ Creating terraform_<指定したTerrafomのバージョン>-bundle<YYYYMMDDHH>_linux_amd64.zip ... All done! Bundle ファイルの登録 これで Bundle ファイルは完成したので、これを閉域環境に持ち込みます。 最後に Admin Terraform Versions API を利用して、作成した組織に登録していきます。 1. Bundle ファイルの配置 Bundle ファイルを、TFE が認証なくアクセス可能な WEB サーバ上に配置します。 2. URL および checksum 結果の取得 以降の手順で使用するため、Bundle を取得可能な URL と、Bundle の SHA-256 checksum 結果 ( sha256sum <Bundle-PATH>|awk '{print $1}' ) を取得します。 3. Bundle ファイルの登録 (新規登録) cat <<EOF >payload.json { "data": { "type": "terraform-versions", "attributes": { "version": "指定したTerrafomのバージョン", "url": "Bundleファイルの配置先URL", "sha": "BundleファイルのSHA-256 checksum結果", "official": true, "enabled": true, "beta": false } } } EOF curl \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/vnd.api+json" \ --request POST\ --data @payload.json \ http://tfe.xxx.internal/api/v2/admin/terraform-versions これにより Terrafom 実行時の Provider が作成した Bundle ファイルを使用するようになるため、実行可能になります。 終わりに この記事では、Terraform Enterprise の構築手順と設定について説明しました。 TFE を使用することで、インフラストラクチャの自動化と可視化、チーム作業の効率化とセキュリティの向上など様々な効果が期待されます。 閉域網でも活用することが可能なことが分かったので、今回紹介した設定以外の見直しも含め、今後も積極的に活用していければと思います。 最後までご覧いただきありがとうございました。本記事がどなたかの一助となれれば幸いです。 浅香 樹 みずほリサーチ&テクノロジーズ 先端技術研究部に所属。2019年から、CCoEとしてAWSの社内利活用の推進に従事。 Google Cloudは2022年より利用開始し、それを機にTerraformにも取り組んでいます。
G-gen の杉村です。Cloud Logging のログバケットを作成する際に リクエストしたエンティティは見つかりませんでした というメッセージが出力されました。原因と対処法を紹介します。 はじめに・前提知識 事象 原因調査 調査 判明した原因 対処法 はじめに・前提知識 Cloud Logging の ログバケット はログを保管することに特化した Cloud Logging 独自のストレージです。「Cloud Storage バケット」とは名称がよく似ていますが関係がありません。 Cloud Logging の詳細な解説は、以下の記事をぜひご参照ください。 blog.g-gen.co.jp 事象 ある組織配下のプロジェクトにおいて、Cloud Logging コンソールで新しいログバケットを作成することを試みました。 作成ボタンを押すと、以下のメッセージが表示され、作成することができませんでした。 リクエストしたエンティティは見つかりませんでした。 ログバケット作成時のエラーメッセージ このメッセージだけでは、何を意味しているのか分かりません。詳細なエラーメッセージを得るために、今度は gcloud コマンドでのログバケット作成を試しました。すると、以下のような出力となりました $ gcloud logging buckets create my-log-bucket --location=global --project=my-current-project ERROR: ( gcloud.logging.buckets.create ) NOT_FOUND: Project does not exist: my-old-project エラーメッセージ中の my-old-project は仮名ですが、既に削除済みのプロジェクトでした。これは何を意味しているのでしょうか。 原因調査 調査 gcloud コマンドを実行したときのエラーログに着目します。 ERROR: (gcloud.logging.buckets.create) NOT_FOUND: Project does not exist: my-old-project ログバケット作成先のプロジェクトは my-current-project を指定しているのにも関わらず、エラーメッセージは my-old-project (仮名) が存在していない、というものです。社内で my-old-project のかつての管理者に確認したところ、次に示すことが分かりました。 判明した原因 プロジェクト my-old-project は、検証目的で Cloud KMS 鍵を配置していたプロジェクトでした。また、この事象が起きた日の少し前に、 組織レベル でログバケットの CMEK 暗号化を有効化する検証を行っていました。 参考 : ログ ストレージ用の CMEK を構成する CMEK 暗号化とは Customer-Managed Encryption Key の略であり、Google 管理ではなく独自管理の鍵でストレージを暗号化する機能のことです。Cloud Logging では組織レベルで CMEK を使うように指定することで、それ以降にその組織で作成される全てのログバケットが指定の KMS キーで暗号化されるようになります。 この事象が起きた組織では、直前にこの機能の挙動の検証を行っており、その際の暗号化用 KMS キーとして my-old-project 内の KMS キーを指定していました。検証が終わり、 my-old-project は削除されましたが、 組織レベルの CMEK 暗号化設定を削除し忘れ ていました。 そのためログバケットを新規作成しようとした際、Google は CMEK 暗号化のための KMS キーを my-old-project に探しに行き、プロジェクトが存在しないため NOT_FOUND: Project does not exist というメッセージを出力したのです。 対処法 以下のコマンドで、現在の設定を確認します。 ${ORGANIZATION_ID} は自身の組織 ID に置き換えます。 $ gcloud logging settings describe --organization= ${ORGANIZATION_ID} { " kmsKeyName " : " projects/my-old-project/locations/asia-northeast1/keyRings/audit-log-keyring/cryptoKeys/bucket-cmek " , " kmsServiceAccountId " : " cmek-o123456789012@gcp-sa-logging.iam.gserviceaccount.com " , " name " : " organizations/123456789012/settings " , " storageLocation " : " asia-northeast1 " } 設定値 kmsKeyName が存在しており、CMEK 暗号化を強制する設定が残っていることが分かります。以下のコマンドで削除します。 $ gcloud logging settings update --organization= ${ORGANIZATION_ID} --clear-kms-key ログバケットのデフォルトのロケーションも、デフォルトである global に戻しておきます。 $ gcloud logging settings update --organization= ${ORGANIZATION_ID} --storage-location=global 以下のコマンドで、設定 kmsKeyName が消えていることを確認します。 $ gcloud logging settings describe --organization= ${ORGANIZATION_ID} { " kmsServiceAccountId " : " cmek-o123456789012@gcp-sa-logging.iam.gserviceaccount.com " , " name " : " organizations/123456789012/settings " , " storageLocation " : " global " } 設定を修正後は、無事にログバケットの作成が成功しました。 杉村 勇馬 (記事一覧) 執行役員 CTO / クラウドソリューション部 部長 元警察官という経歴を持つ現 IT エンジニア。クラウド管理・運用やネットワークに知見。AWS 12資格、Google Cloud認定資格11資格。Twitter では Google Cloud や AWS のアップデート情報をつぶやいています。 Follow @y_sugi_it
G-gen の堂原です。マネージドな Apache Airflow 環境を提供する Cloud Composer について、Cloud Composer 2 を中心に解説します。 はじめに Airflow とは 概要 特徴 様々なサービス・ツールと連携可能 定期実行されるバッチ方式のワークフローに特化 可視性 Cloud Composer とは 概要 メリット 苦手な点 バージョン 概要 メジャーバージョン ライフサイクル アップグレード コンポーネント 概要 Airflow ワーカー Airflow スケジューラ Airflow ウェブサーバ Airflow トリガラー Airflow データベース 料金 前提 種類 Cloud Composer コンピューティング料金 Airflow データベースのストレージ料金 コアインフラストラクチャ料金 コスト最適化 セキュリティ サービスアカウント プライベート環境 耐障害性 スナップショット 復元力モード モニタリング はじめに Cloud Composer は Google Cloud (旧称 GCP) のフルマネージドな、データパイプライン用のワークフロー管理サービスで、 実体は Apache Airflow をマネージドな環境で提供するサービスです。 そのため、本記事ではまず Airflow について紹介し、その後に改めて Cloud Composer (メジャーバージョン 2) について紹介します。 ※ 一部 Airflow のコンポーネントなどは Cloud Composer の方で解説しています。 Airflow とは 概要 Airflow は、 元は Airbnb 社で開発され現在は Apache Software Foundation のプロジェクトとなっている OSS で、データパイプライン用のワークフロー管理ツールです。 例えば下図のように task_1 の後に task_2 を処理する task_2 処理後、 task_3 と task_4 が並列で処理される task_3 と task_4 処理後、 task_5 が処理される などといった処理の順序や依存関係を組み立てることができます。 ワークフローの例 Airflow においては、個々の処理 ( Task と呼ばれます) の順番や依存関係を DAG (Directed Acyclic Graph) というもので定義します。 また、各 Task は Operator と呼ばれるテンプレートを用いて作成することができます。 これらは Python で記述され、上図を実現する Python コードは以下のようになります。 (下のコードだと、各 Task はただ echo を実行しているだけですが、実際はここに ETL (Extract, Transform, Load) 処理を当てはめていくことになります。) import airflow from airflow import DAG from airflow.operators.bash import BashOperator from airflow.utils.dates import days_ago with DAG( 'test-dag' , description= 'test dag' , schedule_interval= '*/10 * * * *' , start_date=airflow.utils.dates.days_ago( 1 ), catchup= False , max_active_runs= 1 ) as dag: task_1 = BashOperator( task_id= 'task_1' , bash_command= 'echo "Task 1"' , dag=dag) task_2 = BashOperator( task_id= 'task_2' , bash_command= 'echo "Task 2"' , dag=dag) task_3 = BashOperator( task_id= 'task_3' , bash_command= 'echo "Task 3"' , dag=dag) task_4 = BashOperator( task_id= 'task_4' , bash_command= 'echo "Task 4"' , dag=dag) task_5 = BashOperator( task_id= 'task_5' , bash_command= 'echo "Task 5"' , dag=dag) task_1 >> task_2 >> [task_3, task_4] >> task_5 特徴 様々なサービス・ツールと連携可能 Airflow は様々なツールやサービスと連携し、ETL 処理を実行することが可能です。 データの取得・格納先として、例えば Google Cloud であれば BigQuery や Google Cloud Storage (GCS) 、AWS であれば Redshift や S3 、その他各種データベース ( MySQL , PostgreSQL , MongoDB など) が選択できます。 データの処理基盤としては、大規模なデータの処理が行える OSS である Apache Beam や、 Kubernetes などが選択でき、Python で記述した簡単な処理であれば Airflow の基盤上で実行するといったことも可能です。 クラウドサービスを使う場合、Cloud SDK や AWS SDK for Python (Boto3) 等を使って各処理を書き込む必要はなく、 用途に合った Operator を選択し、各パラメータを記載するだけで Task を作成し処理を実現することができます。 (勿論、権限設定やライブラリのインストール自体は必要となります。) 例えば BigQuery テーブルからデータを取得したい場合は BigQueryGetDataOperator を用いて Task を作成します。 get_data_from_bq = BigQueryGetDataOperator( task_id = 'get_data_from_bq' , project_id= 'test-project' , dataset_id = 'test_dataset' , table_id = 'test_table' , dag = dag ) 定期実行されるバッチ方式のワークフローに特化 オンデマンドに実行することも可能ですが、Airflow は 定期的に実行されるバッチ方式のワークフロー に特化しています。 (逆に言えば Airflow はストリーミング処理には適していません。) そのため、Airflow では定期実行を意識した機能が用意されています。 例えば バックフィル機能 を使えば、スタート時点から、DAG で定めた間隔分だけワークフローを実行してくれます。 例 : スタート時点を一ヶ月前・間隔を 1 日にすると、過去 30 日分のワークフローを順に実行 当日に追加されたデータのみを処理するといったワークフローを実装している場合、本機能を用いることで、過去分のデータまで処理させるといったことが可能になります。 可視性 Airflow では、DAG の設定や実行、個々のタスクの実行結果の確認などができる Airflow UI が提供されています。 下図は、DAG のこれまでの実行結果を確認できるページです。 Airflow UI この他にも DAG のリアルタイムの実行状況 カレンダー形式での日次の実施状況 各実施結果のログ などの情報が Airflow UI で確認可能であり、優れたモニタリング機能となっています。 Cloud Composer とは 概要 改めて、 Cloud Composer は Google Cloud (旧称 GCP) のフルマネージドな、Apache Airflow 環境を提供するサービスです。 ユーザが DAG を GCS にアップロードすると、Cloud Composer がワークフローの実行や計算リソースのスケーリングを管理してくれます。 基本的な利用の流れは以下のようになります。 バージョンや計算リソースのスペックを指定して Cloud Composer 環境 を作成 (以後、「環境」と記載する場合は Cloud Composer 環境を指します) 計算リソースのスペックは後から変更可能です 環境毎に作成される GCS に DAG を記載した Python ソースコードをアップロード Cloud Composer が DAG を自動で検知し、記載されたトリガーに従ってワークフローの実行・管理を行う また、以下のようなことを Google Cloud コンソールから確認・実施することが可能と、高い利便性が提供されています。 各 DAG のグラフやソースコードが確認可能 Airflow UI へのアクセス Airflow の構成オプションや環境変数、PyPI パッケージの上書きや追加・変更 メリット 先述した Airflow の特徴も踏まえ、Cloud Composer には以下のようなメリットが存在します。 Python で、複雑なパイプラインを簡単に構築することができる Google Cloud の複数のサービスを横断するパイプラインの構築が可能 Google Cloud だけでなく、ハイブリッドやマルチクラウドにも対応 例 : S3 から GCS を経由して BigQuery にデータ保存などといったパイプラインを構築可能 複雑な Airflow 環境を簡単に立ち上げられる 計算リソースの自動スケーリング機能あり(Cloud Composer 2 のみ) ただしサーバレスではないため、各コンポーネント (後述) のスペックのチューニングは必要となります 苦手な点 Cloud Composer は、逆に以下のような点を苦手としています。 Python でのパイプライン構築が必須 簡単なパイプラインを非エンジニアの方が作成したいのなら、 Dataprep や Cloud Data Fusion といったノーコードサービスのほうが適しています ストリーミング処理 Google Cloud なら、 Dataflow が適任です バージョン 概要 Cloud Composer においては高頻度で新しいバージョンがリリースされています。 各バージョンは composer-a.b.c-airflow-x.y.z という形式で表され、 a.b.c が Cloud Composer のバージョンで x.y.z が Airflow のバージョンとなります。 基本的に Cloud Composer の 1 つのバージョンにつき、2 つの Airflow マイナーバージョンがサポートされます。 例えば Cloud Composer 2.2.1 であれば、Airflow 2.5.1 と Airflow 2.4.3 をサポートします。 参考 : Cloud Composer のバージョニングの概要 メジャーバージョン Cloud Composer においては 2 つのメジャーバージョン、Cloud Composer 1 と Cloud Composer 2 が存在します。 数字の通り Cloud Composer 2 が後継です。 機能面の大きな違いとして Cloud Composer 1 は後述する Airflow ワーカーの スケーリングが手動 で、Cloud Composer 2 は 自動スケーリング となっています。 また、 Cloud Composer 1 は Standard モードの Google Kubernetes Engine (GKE) クラスタで実装されている一方、Cloud Composer 2 は Autopilot モードの GKE クラスタで構築されており、アーキテクチャ的にも大きく異なっています。 Cloud Composer 1 は、2023 年 3 月 24 日にリリースされたバージョン 1.20.11 が最後で、今後はバグの修正と小規模な改善のみが行われます。 そのため、現在 Cloud Composer を使用する場合は、Cloud Composer 2 の使用が推奨されます。 公式ドキュメントにおいては、Cloud Composer 1 用のページと Cloud Composer 2 用のページが用意されているので、間違えないようご注意ください。 ライフサイクル Cloud Composer の各バージョンはリリース日を基準として、以下のようなサポート期間が設けられています。 0 - 12 ヶ月 : 完全サポート 12 - 18 ヶ月 : セキュリティに関する通知のみ行われる 18 ヶ月 - : 全てユーザ管理 常にサポート対象のバージョンにアップグレードすることが推奨されてはいますが、2023 年 6 月時点では、サポート期間終了後も強制アップグレードは実施されず、同じバージョンを使用し続けることが可能です。 アップグレード 2023 年 6 月時点ではプレビュー機能となっていますが、Cloud Composer においては環境のアップグレード機能が提供されています。 また、アップグレードによって PyPI パッケージの競合が発生しないかを事前に確認することも可能です。 ただし先述した通り、本機能はプレビュー機能となっているため、 本番環境で使用する前にしっかり検証しておく必要があります 。 参考 : 環境をアップグレードする コンポーネント 概要 ここからは改めて Cloud Composer 2 についての解説となります。 Cloud Composer 2 は複数の Google Cloud サービス上に存在する複数のコンポーネントから成り立っていますが、主に意識する必要のあるコンポーネントは以下の通りです。 コンポーネント コンポーネントを実装している Google Cloud サービス Airflow ワーカー Autopilot モードの GKE クラスタ Airflow スケジューラ Autopilot モードの GKE クラスタ Airflow ウェブサーバ Autopilot モードの GKE クラスタ Airflow トリガラー Autopilot モードの GKE クラスタ Airflow データベース Cloud SQL インスタンス 環境用バケット GCS バケット 上表のコンポーネントを実装している Google Cloud サービスの内、GKE クラスタと GCS については Google Cloud コンソールから確認が可能です。 ただし、環境が壊れる可能性があるため、GKE クラスタの変更は非推奨です。 参考 : 環境コンポーネント Airflow ワーカー Airflow ワーカーは、環境に存在する DAG の個々の Task を実行する、Cloud Composer における計算リソースとなります。 Task 量に応じて自動でスケーリングが行われます。 各ワーカーのスペック (vCPU、メモリサイズ、ストレージサイズ) と、スケーリングにおけるワーカーの最小数と最大数が指定できます。 Airflow スケジューラ Airflow スケジューラは、環境に存在する DAG の実行と、各 DAG 個々の Task の実行のスケジューリングを制御します。 個々の Task は GKE クラスタ内に実装されているキューを通して、スケジューラからワーカーへ分配されていますが、ユーザがキューを意識する必要はほとんどありません。 スケジューラにおいては、スケジューラのスペック (vCPU、メモリサイズ、ストレージサイズ) と、実行するスケジューラの数を指定できます。 Airflow ウェブサーバ Airflow ウェブサーバは Airflow UI を提供するコンポーネントです。 ウェブサーバはスペック (vCPU、メモリサイズ、ストレージサイズ) を指定できます。 Airflow UI へのアクセス権限は IAM を用いて管理することが可能で、アクセスには composer.environments.get 権限が必要です。 また、指定した CIDR からのみのアクセスを許可する、IP アドレスベースでのアクセス制御も可能です。 参考 : UI / Screenshots 参考 : Airflow UI のアクセス制御の使用 参考 : 手順 8. (省略可)ウェブサーバーへのネットワーク アクセスを構成する Airflow トリガラー Airflow トリガラーは、Airflow の Deferrable Operators (Google Cloud の日本語ページだと「遅延可能な演算子」と記載されています) という機能の管理に用いられます。 通常、BigQuery ジョブを呼び出したりと、外部のシステム上で処理行うような Task を実行している最中も、その Task はワーカースロットを占有してしまいます。 Deferrable Operators は、そのような処理の監視をトリガラーが代わりに行うことで、ワーカースロットを解放することができる機能です。 Cloud Composer においてはトリガラーの数は 0 にすることも可能で、 トリガラーの数を 1 以上にすることで Deferrable Operators が有効化されます 。 また、トリガラーのスペック (vCPU、メモリサイズ、ストレージサイズ) も指定できます。 Airflow データベース Airflow データベースは、Airflow のメタデータデータベースをホストしています。 データベースの実体である Cloud SQL インスタンスは Google の方で完全に管理されており、Google Cloud コンソールでは確認することもできません。 データベースのスペック調整は、Airflow スケジューラで触れた Airflow キューなどとひとまとめにされた、 コアインフラストラクチャ 単位で行われます。 コアインフラストラクチャは「小、中、大」の 3 つの環境サイズが用意されており、DAG の数や規模感に応じて選択することになります。 ただし、データベースのディスク容量のみ、必要に応じて自動的に増加します。 参考 : 環境サイズを変更する 参考 : データベースのディスク容量 料金 前提 Cloud Composer 2 の料金体系を理解するのは、これまで紹介した内容を踏まえ、以下の点を理解しておく必要があります。 Cloud Composer 1 と Cloud Composer 2 では料金体系が大きく異なる Airflow ワーカー、スケジューラ、ウェブサーバ及びトリガラーは vCPU、メモリサイズ、ストレージサイズについて調整が可能 Airflow データベースや Airflow キューなどのコンポーネントはまとめてコアインフラストラクチャという単位でサイズ調整を行う ただし、データベースのディスク容量のみ、必要に応じて自動的に増加 種類 Cloud Composer 2 においては、主に以下の料金が発生します。 Cloud Composer コンピューティング料金 Airflow データベースのストレージ料金 コアインフラストラクチャ料金 ただし、環境毎に用意される GCS バケットや、後述するプライベート環境を構築する際に使用される Private Service Connect についても別途料金が発生します。 料金表は以下の公式ページを参照ください。 参考 : Cloud Composer 2 の料金表 Cloud Composer コンピューティング料金 Cloud Composer コンピューティング料金は、Airflow ワーカー、スケジューラ、ウェブサーバ及びトリガラーがそれぞれ使用している vCPU、メモリ、ストレージに対して発生する料金です。 ワーカーやスケジューラ、トリガラーについては、指定したスペックに起動数を掛けた値となります。 vCPU、メモリサイズ、ストレージサイズはそれぞれ個別に計算され、時間単位で料金が発生します。 Airflow データベースのストレージ料金 Airflow データベースのディスク容量は必要に応じて自動的に増加し、そのサイズに応じて料金が発生します。 コアインフラストラクチャ料金 コアインフラストラクチャ料金は、コアインフラストラクチャの 3 つの環境サイズ 「小、中、大」に応じて発生する料金です。 後述する復元力モードが「標準的な復元力」か「高い復元力」によっても料金が異なり、「高い復元力」の方が料金は高くなります。 コスト最適化 Cloud Composer の費用を最適化する方法が、以下で紹介されています。 参考 : 環境のパフォーマンスと費用を最適化する Cloud Composer 2 においては、まず「小、中、大」の粒度のプリセットで用意されているスペックのいずれかで利用を開始します。 ※ 「プリセットスペック = Airflow ワーカー、スケジューラ、ウェブサーバ及びトリガラーのスペック + コアインフラストラクチャの環境サイズ」であり、コアインフラストラクチャの環境サイズ「小、中、大」とイコールではありません。 その後、実際のワークロード (DAG) を実行してパフォーマンスを観察します。後述の「モニタリング」の項で紹介する通り、Composer のパフォーマンスは Cloud Monitoring で自動的に収集されています。その収集結果を見ながら、各コンポーネントのスペックを調整していきます。その際は、Airflow ワーカーのみスペックを下げるなど、個別に調整することも可能です。 セキュリティ サービスアカウント Cloud Composer においては、2 つのサービスアカウントが登場します。 環境のサービスアカウント Cloud Composer のサービスエージェントアカウント 環境のサービスアカウントは環境毎に設定するもので、環境内のワークフローはこのサービスアカウントの権限を使用して他の Google Cloud サービスにアクセスします。 環境のサービスアカウントには、デフォルトの Compute Engine サービスアカウントまたはユーザが作成したサービスアカウントを用いることが可能です。 ユーザが作成したサービスアカウントを用いる場合は、そのサービスアカウントに Composer ワーカー ロールを付与する必要があります。 一方、Cloud Composer のサービスエージェントアカウントは Google が管理しており、プロジェクト内の全ての環境で使用されています。 環境作成時は Cloud Composer のサービスエージェントアカウントを用いて Workload Identity 設定を行う都合上、 Cloud Composer のサービスエージェントアカウントに環境のサービスアカウントに対する Cloud Composer v2 API サービス エージェント拡張機能ロールを付与する必要があります 。 まとめると、環境構築時、基本的には以下の権限を各サービスアカウントに付与する必要があります。 環境のサービスアカウント : プロジェクトに対する Composer ワーカーロール Cloud Composer のサービスエージェントアカウント : 環境のサービスアカウントに対する Cloud Composer v2 API サービス エージェント拡張機能ロール 参考 : IAM を使用したアクセス制御 プライベート環境 通常 Cloud Composer においては、GKE クラスタと Cloud SQL インスタンスに対してパブリック IP アドレスが付与されます。 プライベート環境設定を行うと、GKE クラスタは限定公開クラスタとして作成され、また Cloud SQL はプライベート IP アドレスのみを有するようになり、セキュアな環境を構築できます。 限定公開クラスタについては以下の記事で紹介しています。 blog.g-gen.co.jp また、プライベート環境設定においても以下のオプションがあります。 Google 管理の VPC (Cloud SQL インスタンスが存在) とユーザ管理の VPC (GKE クラスタが存在) 間の接続方法 Private Service Connect VPC ピアリング GKE クラスタのコントロールプレーンのパブリックエンドポイントの有効・無効化 これらの設定は環境作成時のみに設定可能で、作成後の変更はできません。 プライベート環境にした場合、ワークフローから外部ネットワークに接続するためには Cloud NAT 等が必要となります。 また、GKE クラスタのコントロールプレーンのパブリックエンドポイントを無効にした場合、ローカル環境からの Airflow CLI コマンド の実行ができなくなります。 もう一点注意点として、 Airflow ウェブサーバ (Airflow UI) へのアクセスはプライベート環境にしても制限されません 。 参考 : プライベート IP 環境 耐障害性 スナップショット Cloud Composer においては、以下のようなデータのスナップショットを定期的にまたはオンデマンドに保存し、環境の復元ができます。 Airflow 構成オプションの上書き、環境変数、PyPI パッケージ Airflow データベースのバックアップ 環境用 GCS バケットの /dag 、 /data 及び /plugins フォルダ スナップショットはデフォルトでは環境用 GCS バケットの /snapshots フォルダに保存されますが、宛先の変更も可能です。 参考 : 環境のスナップショットの保存と読み込み 復元力モード Cloud Composer においては、復元力モードというものが指定できます。 復元力モードを「高い復元力」とした場合、環境が複数のゾーンを跨る形で構築され耐障害性が増します。 具体的には以下のような構成になります。 2 つ以上の Airflow ワーカー及び 2 つの Ariflow スケジューラ、ウェブサーバ、トリガラーが異なるゾーンに配置される Airflow データベースを構成する Cloud SQL インスタンスは 高可用性モード で実行される 注意点として、「高い復元力」は プライベート環境のみ で選択可能です。 また、コアインフラストラクチャに対して通常より高い料金が発生します。 参考 : 復元性に優れた Cloud Composer 環境を設定する モニタリング Cloud Composer は Cloud Monitoring と統合されており、各コンポーネントの CPU 使用率、メモリ使用量などのメトリクスが自動的に収集されています。 これにより、コンソール画面で Airflow ワーカー、スケジューラ、ウェブサーバ、トリガラーおよびデータベースの稼働状況を確認することができます。 Cloud Composer においては各コンポーネントのスペックを後から変更することが可能なため、これらの稼働状況は適切なスペックを判断するのに役立ちます。前述した「コスト最適化」の項もご参照ください。 モニタリング画面 堂原 竜希 (記事一覧) データアナリティクス準備室。2023年4月より、G-genにジョイン。 Google Cloud Partner Top Engineer 2023, 2024に選出 (2024年はRookie of the yearにも選出)。休みの日はだいたいゲームをしているか、時々自転車で遠出をしています。 Follow @matayuuuu
当記事は みずほリサーチ&テクノロジーズ × G-gen エンジニアコラボレーション企画 で執筆されたものです。 みずほリサーチ&テクノロジーズ株式会社の舘山と申します。 当記事では Cloud Monitoring 指標スコープ を活用して複数プロジェクトを横断してリソースの消費状況をモニタリングする事例を紹介します。 当ブログは G-gen × みずほRT によるコラボ記事です はじめに 複数プロジェクトとオンプレミス環境の接続 個別システムと共通基盤の役割分掌 VPC ピアリンググループ単位の制限 共通基盤によるモニタリング 前提知識 割り当てと上限 VPC ピアリングに関する割り当てと上限 ピアリンググループ単位の割り当てのモニタリング アプローチ 指標スコープの設定 制限事項 カスタムダッシュボードの作成 アラートポリシーの作成 さいごに はじめに まずはじめに、なぜ Cloud Monitoring 指標スコープ を用いて VPC ピアリンググループ単位の割り当て消費をモニタリングする必要があるかをご説明します。 複数プロジェクトとオンプレミス環境の接続 マルチテナントのように数多くの Google Cloud のテナントプロジェクトとオンプレミス環境を接続する場合、以下のような VPC ピアリングによるハブアンドスポーク構成が有力な選択肢です。 共有 VPC 構成よりも堅確にテナントネットワーク相互の分離、アクセス制御を実現できる構成です。 ハブアンドスポーク構成 個別システムと共通基盤の役割分掌 ここで、テナントプロジェクトの利用者(個別システム)と提供者(共通基盤)の役割分掌に注目します。一般的には VPC と VPC ピアリング接続の作成 組織内でユニークなプライベート IP アドレス範囲の割り当て などは個別システムからの申請に基づき共通基盤が実施します。 一方で VM インスタンスの作成 サブネットの作成 などは個別システムに権限移譲したほうが開発速度を加速できるでしょう。 VPC ピアリンググループ単位の制限 ここで 1 点注意が必要なのですが、 VM インスタンスやサブネットを無限に作成できるわけではありません。 本件のようなマルチテナントの場合、中心となるハブプロジェクトから各テナントプロジェクトに VPC ピアリングしているため、VPC ピアリンググループ(中心となる VPC と直接ピアリング接続された VPC の集合)単位の制限に注意が必要です。制限を超過して VM インスタンスやサブネットを作成することはできません。 ハブアンドスポーク構成(再掲) テナントプロジェクト側は、自 VPC の割り当ては意識できていても、ピアリンググループ単位の割り当ては把握できません。 そのような状況で、ある日突然 VM インスタンスやサブネットを作成できなくなると問題です。 制限に抵触しないように共通基盤がピアリンググループ単位の利用状況をモニタリングし、必要に応じて割り当て量の増加をリクエストする運用を行います。 共通基盤によるモニタリング そこで共通基盤は Cloud Monitoring 指標スコープ を用いて VPC ピアリンググループ単位の割り当て消費をモニタリングする 運用が必要となってくるわけです。 なお、複数プロジェクトのモニタリングについて、指標スコープと従来のワークスペースとの差異については、以下のブログ記事の解説が参考になります。 参考: 複数プロジェクト構成の Cloud Monitoring がより使いやすくなりました 前提知識 割り当てと上限 繰り返しになりますが、Google Cloud の各サービスでは無制限にリソースを作成できるわけではありません。 制限として割り当てと上限があります。両者の違いは以下のようになります。 観点 割り当て 上限 増加の可否 ほとんどの場合、コンソールの割り当て画面から 割り当ての増加 が可能。 原則不可。 例外的にサポートケース起票で上限の引き上げができる項目が存在。 Cloud Monitoringの指標による消費状況の確認 ほとんどの場合、 Cloud Monitoringの割り当て指標 で、現在の割り当て消費状況を確認可能。 コンソールの割り当て画面で、割り当て指標を一括管理可能。 Cloud Monitroingで上限の消費状況を確認できる指標は提供されていない。 なお、クラウドにおけるリソース数などの制限の管理については、AWSのドキュメントになりますが、AWS Well-Architected Framework信頼性の柱の記述が参考になります。 参考: REL 1 サービスクォータと制約はどのように管理しますか? VPC ピアリングに関する割り当てと上限 「はじめに」でも述べましたが、VPC ピアリンググループ(中心となる VPC と直接ピアリング接続された VPC の集合)単位の VM インスタンス数、サブネット数などには制限があります。 参考: VPC の割り当てと上限のガイド VM インスタンス数やサブネット数などは割り当てに分類されており、消費状況を確認できます。 例:ピアリンググループあたりのサブネット数の割り当て指標 compute.googleapis.com/quota/subnet_ranges_per_peering_group/limit compute.googleapis.com/quota/subnet_ranges_per_peering_group/usage また VPC 単位の割り当ても存在します。 例:VPCあたりのサブネット数の割り当て指標 compute.googleapis.com/quota/subnet_ranges_per_vpc_network/limit compute.googleapis.com/quota/subnet_ranges_per_vpc_network/usage ピアリンググループ単位の割り当てのモニタリング 今回は、 ハブアンドスポークアーキテクチャ を想定し、ハブ VPC を中心としたピアリンググループをモニタリング対象とします。 アプローチ ピアリンググループ全体の割り当て消費だけでなく、VPC 毎の割り当て消費の内訳もモニタリングできることが望ましいです。 そのため、ピアリンググループ単位の割り当て指標を直接モニタリングするのではなく、VPC 単位の割り当て指標を集計するアプローチを採用します。 また、ピアリンググループを構成する VPC が複数プロジェクトに跨っている場合にも対応する必要があります。 複数プロジェクト横断のモニタリングには、Cloud Monitoring 指標スコープを利用します。 指標スコープの設定 指標スコープの設定方法は、Cloud Monitoring のガイドを参照してください。 参考: 複数の Cloud プロジェクトの指標を表示する 指標スコープ設定例 下の画像は、指標スコープの設定後にメトリクスエクスプローラーで、VPC 単位の サブネット数の割り当て指標(compute.googleapis.com/quota/subnet_ranges_per_vpc_network/usage)を参照した例です。 指標スコープ設定後のメトリクスエクスプローラー 指標スコープ内の全プロジェクトの VPC の指標が参照されます。 ピアリンググループに参加しない VPC がある場合、フィルター条件で当該 VPC を対象から除外します。 更新がない状態で指標は 1 日に 1 回しか書き込まれていなかったため、アライメント期間を 1500m(25 時間)に設定し、期間内に値が複数存在する場合は、最新の値(next older)を採用しています。 なお、グラフで VPC 毎の利用状況を把握するため Aggregator は none を指定しています。 制限事項 Cloud SQL 等の利用時、Google 管理の VPC との VPC ピアリング接続が作成されます。 Google 管理の VPC は利用者のプロジェクトに属さないため、指標スコープを通して集計対象に含めることはできません。 カスタムダッシュボードの作成 Cloud Monitoring ではカスタムダッシュボードを作成できます。 参考: カスタム ダッシュボードの作成と管理 ピアリンググループの割り当て管理用のカスタムダッシュボードとして、割り当て毎に下の画像のようなグラフを配置する構成が考えられます。 ピアリンググループのカスタムダッシュボードの例 VPC 毎の内訳と推移のグラフは、前掲のメトリクスエクスプローラのように、VPC 単位の割り当て指標を参照します。 ピアリンググループ単位の現在の割り当てと消費数のスコアカードは、次のアラートポリシーのようにピアリンググループ単位の割り当て指標を直接参照します。 アラートポリシーの作成 Cloud Monitoring ではアラートポリシーを定義して、割り当て消費が閾値を超過した場合にメール通知できます。 アラートポリシーの作成手順については、Cloud Monitoring のガイドを参照してください。 参考: 指標ベースのアラート ポリシーを作成する 今回は Terraform でピアリンググループ単位の VM インスタンス数のアラートポリシーを定義してみます。 このユースケースでは、VPC 毎の内訳は不要のため、ピアリンググループ単位の割り当て指標(instances_per_peering_group/usage)を直接参照できます。 指標スコープ内にはスポーク VPC を中心としたピアリンググループの割り当て指標も存在するため、フィルタ条件でハブ VPC を指定します。 resource "google_monitoring_alert_policy" "instance-per-peering-group" { display_name = "instance-per-peering-group" combiner = "OR" conditions { display_name = "VPC Peering - Instances Per Peering Group quota usage" condition_threshold { filter = <<EOF resource.type = "compute.googleapis.com/VpcNetwork" AND metric.type = "compute.googleapis.com/quota/instances_per_peering_group/usage" AND resource.labels.network_id = $ { local.hub-vpc } EOF threshold_value = 10000 trigger { count = 1 } duration = "0s" comparison = "COMPARISON_GT" aggregations { alignment_period = "90000s" per_series_aligner = "ALIGN_MAX" } } } notification_channels = [ google_monitoring_notification_channel.email.id ] } アラートポリシーの閾値を下げて、アラートメールを受信してみます。 test さいごに VPC ピアリンググループの割り当てのモニタリングに Cloud Monitoring 指標スコープを活用する事例の共有でした。 Cloud Monitoring 指標スコープのユースケースとして参考にしていただければと思います。 舘山 浩之 みずほリサーチ&テクノロジーズ 先端技術研究部に所属。個人のキャリアではAWSの利用経験が長く、Google Cloudは2022年より利用開始。