Published on

ハンズオンメモ:Chapter 7: Working with Data

はじめに

この記事は以下書籍の勉強メモです。
image.png (118.4 kB)
image.png (118.4 kB)

やったこと

※書籍の該当章に書かれている内容を1つのハンズオンで確認できるようにGeminiでまとめ直したものです。

Chapter 7: Working with Data ハンズオン

本ハンズオンでは、Google Cloud におけるデータ操作の核心となる、Cloud Storage の最適化、Compute Engine でのデータ保護、そしてスケーラブルなデータベース(Spanner/Firestore)の活用方法を、一連の流れで学習します。

リソース構成図

準備:環境セットアップ

作業を開始する前に、認証情報の更新とディレクトリ作成を行います。
# 認証の実行
gcloud auth login
gcloud auth application-default login

# 作業ディレクトリの作成
mkdir terraform
mkdir app
cd terraform

ステップ 1:インフラストラクチャの構築(ネットワーク・IAM)

このステップでは、ハンズオンの土台となる VPC ネットワーク(Azure Virtual Network 相当)、サブネット、および外部 IP を持たない VM がインターネット通信を行うための Cloud NAT(Azure NAT Gateway 相当)を構築します。また、最小権限の原則に基づき、VM 用のサービスアカウント(Azure Managed Identity 相当)を作成します。
概要:
組織ポリシー(外部 IP 禁止)に対応するため、カスタム VPC と Cloud NAT を構築します。VM は外部 IP を持たず、NAT 経由でパッケージをインストールします。また、セキュリティ向上のため専用のサービスアカウントを作成し、必要な権限のみを付与します。
ファイルパス: terraform/main.tf
terraform {
  required_version = "~> 1.9.0"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 6.14.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}

# APIの有効化
resource "google_project_service" "compute" {
  service            = "compute.googleapis.com" # GCEリソース作成用
  disable_on_destroy = false
}

resource "google_project_service" "spanner" {
  service            = "spanner.googleapis.com" # Spanner操作用
  disable_on_destroy = false
}

resource "google_project_service" "firestore" {
  service            = "firestore.googleapis.com" # Firestore操作用
  disable_on_destroy = false
}

# ネットワーク
resource "google_compute_network" "vpc" {
  name                    = "data-handson-vpc"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "subnet" {
  name          = "data-handson-subnet"
  ip_cidr_range = "10.0.1.0/24"
  region        = var.region
  network       = google_compute_network.vpc.id
}

# Cloud NAT
resource "google_compute_router" "router" {
  name    = "nat-router"
  network = google_compute_network.vpc.id
  region  = var.region
}

resource "google_compute_router_nat" "nat" {
  name                               = "nat-config"
  router                             = google_compute_router.router.name
  region                             = var.region
  nat_ip_allocate_option             = "AUTO_ONLY"
  source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
}

# サービスアカウント
resource "google_service_account" "vm_sa" {
  account_id   = "data-handson-sa"
  display_name = "Service Account for Data Handson VM"
}

# 必要なロールの付与
resource "google_project_iam_member" "storage_admin" {
  project = var.project_id
  role    = "roles/storage.admin"
  member  = "serviceAccount:${google_service_account.vm_sa.email}"
}

resource "google_project_iam_member" "spanner_database_user" {
  project = var.project_id
  role    = "roles/spanner.databaseUser"
  member  = "serviceAccount:${google_service_account.vm_sa.email}"
}

ステップ 2:ストレージとデータベースの構築

Cloud Storage バケット(Azure Blob Storage 相当)にライフサイクルルールを設定し、データの自動アーカイブを構成します。また、Cloud Spanner(Azure Cosmos DB Distributed SQL 相当)と Firestore(Azure Cosmos DB NoSQL 相当)をプロビジョニングします。
概要:
GCS バケットを作成し、365 日経過後に Nearline へ移行するライフサイクルを設定します。Spanner では親子関係を持つインターリーブテーブルを作成し、物理的なデータ配置を最適化します。Firestore もネイティブモードで有効化します。
ファイルパス: terraform/storage.tf
# Cloud Storage
resource "google_storage_bucket" "data_bucket" {
  name                        = "data-handson-bucket-251226"
  location                    = "ASIA-NORTHEAST1"
  force_destroy               = true
  deletion_protection         = false # 削除保護を無効化

  lifecycle_rule {
    condition {
      age = 365
      matches_storage_class = ["STANDARD"]
    }
    action {
      type          = "SetStorageClass"
      storage_class = "NEARLINE"
    }
  }
}

# Cloud Spanner
resource "google_spanner_instance" "main" {
  config           = "regional-asia-northeast1"
  display_name     = "Data Handson Instance"
  processing_units = 100 # 最小構成
  deletion_protection = false
}

resource "google_spanner_database" "database" {
  instance = google_spanner_instance.main.name
  name     = "handson-db"
  ddl = [
    "CREATE TABLE Customers (CustomerId INT64 NOT NULL, FirstName STRING(1024), LastName STRING(1024)) PRIMARY KEY (CustomerId)",
    "CREATE TABLE Orders (CustomerId INT64 NOT NULL, OrderId INT64 NOT NULL, OrderTotal FLOAT64) PRIMARY KEY (CustomerId, OrderId), INTERLEAVE IN PARENT Customers ON DELETE CASCADE"
  ]
  deletion_protection = false
}

# Firestore
resource "google_firestore_database" "database" {
  name        = "(default)"
  location_id = "asia-northeast1"
  type        = "FIRESTORE_NATIVE"
  deletion_protection_policy = "DISABLED"
}

ステップ 3:Compute Engine の構築

データ操作のクライアントとなる VM(Azure VM 相当)を作成します。組織ポリシーに従い、Shielded VM を有効にし、外部 IP を付与せずに作成します。
概要:
Shielded VM 設定を有効にした Compute Engine を作成します。外部 IP を持たないため、IAP(Identity-Aware Proxy)経由で SSH 接続します。OS は最新の Ubuntu 22.04 LTS を使用し、起動時に gcsfuse のインストール準備を行うメタデータを設定します。
ファイルパス: terraform/compute.tf
resource "google_compute_instance" "vm" {
  name         = "data-client-vm"
  machine_type = "e2-micro" # 最安価
  zone         = var.zone

  boot_disk {
    initialize_params {
      image = "ubuntu-os-cloud/ubuntu-2204-lts"
      size  = 20 # 最小限のサイズ
    }
  }

  network_interface {
    subnetwork = google_compute_subnetwork.subnet.id
    # access_configを空にすることで外部IPを付与しない
  }

  service_account {
    email  = google_service_account.vm_sa.email
    scopes = ["cloud-platform"]
  }

  shielded_instance_config {
    enable_secure_boot          = true # 組織ポリシー constraints/compute.requireShieldedVm に対応
    enable_vtpm                 = true
    enable_integrity_monitoring = true
  }

  metadata_startup_script = <<-EOT
    #!/bin/bash
    export GCSFUSE_REPO=gcsfuse-focal
    echo "deb http://packages.cloud.google.com/apt $GCSFUSE_REPO main" | tee /etc/apt/sources.list.d/gcsfuse.list
    curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
    apt-get update
    apt-get install -y gcsfuse
  EOT
}

# IAP経由のSSHを許可するファイアウォール
resource "google_compute_firewall" "allow_iap_ssh" {
  name    = "allow-iap-ssh"
  network = google_compute_network.vpc.id

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = ["35.235.240.0/20"] # IAPのIPレンジ
}
ファイルパス: terraform/variables.tf
variable "project_id" {
  type    = string
  default = "cookbook-251226" # プロジェクトID
}

variable "region" {
  type    = string
  default = "asia-northeast1" # デフォルトリージョン
}

variable "zone" {
  type    = string
  default = "asia-northeast1-a" # デフォルトゾーン
}

ステップ 4:リソースのデプロイ

Terraform を実行して、定義したリソースを Google Cloud 上に作成します。
terraform init
terraform apply -auto-approve

ステップ 5:GCS Fuse とデータ転送の最適化

VM にログインし、GCS Fuse を用いたマウントと、gsutil のマルチプロセスオプションによる高速転送を体験します。
概要:
IAP 経由で VM に SSH 接続し、GCS バケットをローカルディレクトリとしてマウントします。その後、gsutil の -m オプションを使用して、公開データセットから大量のファイルを並列転送し、その速度向上を確認します。

5.1:IAP経由でVMにSSH接続

外部IPを持たないVMに接続するため、Identity-Aware Proxy(IAP)経由でSSH接続します。
# IAP経由でVMにSSH接続
# --tunnel-through-iap オプションでIAP経由の接続を指定
gcloud compute ssh data-client-vm `
  --zone=asia-northeast1-a `
  --tunnel-through-iap `
  --project=cookbook-251226
接続が成功すると、VMのシェルプロンプトが表示されます。

5.2:GCS Fuseのインストール確認とマウント

VMに接続後、起動スクリプトでインストールされたgcsfuseが利用可能か確認します。
# gcsfuseがインストールされているか確認
which gcsfuse
gcsfuse --version

# マウントポイントのディレクトリを作成
mkdir -p ~/gcs-mount

# GCSバケットをマウント
# 注意:サービスアカウントの認証情報が自動的に使用されます
gcsfuse data-handson-bucket-251226 ~/gcs-mount

# マウントの確認
df -h | grep gcs-mount
ls ~/gcs-mount

5.3:データ転送の最適化

gsutilのマルチプロセスオプション(-m)を使用して、並列転送による速度向上を確認します。
# 作業ディレクトリの作成
mkdir -p ~/test-data
cd ~/test-data

# 通常のコピー(シングルプロセス)で速度を確認
# 速度を確認したら Ctrl+C で停止して構いません
time gsutil cp -r gs://gcp-public-data-landsat/LC08/01/044/034/* ./

# マルチプロセスによる高速コピー (-m オプション)
# 意図:並列スレッドを使用してネットワーク帯域を最大限活用する
time gsutil -m cp -r gs://gcp-public-data-landsat/LC08/01/044/034/* gs://data-handson-bucket-251226/bulk/

# マウントしたGCSバケットからファイルを確認
ls ~/gcs-mount/bulk/
Tips:
大量の小規模ファイルを扱う場合は gsutil -m が有効ですが、単一の巨大なファイルを扱う場合は Parallel Composite Uploads を検討してください。ただし、複合オブジェクトは CRC32C による整合性チェックが必要になるなどの制約があります。
参考:

ステップ 6:Spanner インターリーブテーブルの検証

Spanner で作成したインターリーブテーブルに対してデータを挿入し、親子関係にあるデータが物理的に近くに配置されることによるクエリの効率性を理解します。
概要:
Spanner のクエリエディタまたは gcloud コマンドを使用して、Customers と Orders テーブルにデータを挿入します。インターリーブ設定により、特定の顧客の注文データはその顧客レコードと同じスプリット(物理的な保存単位)に保存され、結合クエリが高速化されます。
# データの挿入
gcloud spanner databases execute-sql handson-db --instance=data-handson-instance --sql="INSERT INTO Customers (CustomerId, FirstName, LastName) VALUES (1, 'Rui', 'Costa')"

gcloud spanner databases execute-sql handson-db --instance=data-handson-instance --sql="INSERT INTO Orders (CustomerId, OrderId, OrderTotal) VALUES (1, 101, 52.34)"

# インターリーブされたデータのクエリ
gcloud spanner databases execute-sql handson-db --instance=data-handson-instance --sql="SELECT c.FirstName, o.OrderTotal FROM Customers c JOIN Orders o ON c.CustomerId = o.CustomerId WHERE c.CustomerId = 1"
Tips:
インターリーブテーブルは、親子関係が明確で、常に親キーを介して子データにアクセスするユースケースで非常に強力です。一方で、子テーブル単体でのスキャンが多い場合は、物理的な配置が分散されるためパフォーマンスが低下する可能性があります。
参考:

ステップ 7:Firestore セキュリティルールの適用

Firestore のセキュリティルールを更新し、認証されたユーザーが自身のデータのみを操作できるように制限します。
概要:
Firebase コンソールまたは gcloud コマンドを使用して、Firestore のセキュリティルールをデプロイします。request.auth.uid とドキュメント ID を照合するルールを適用することで、マルチテナントアプリケーションにおけるデータ分離のベストプラクティスを実装します。
ファイルパス: app/firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      // 意図:認証済みかつ自身のUIDと一致するドキュメントのみ読み書きを許可
      allow read, update, delete: if request.auth != null && request.auth.uid == userId;
      allow create: if request.auth != null;
    }
  }
}

ステップ 8:動作確認ハンズオン項目

リソースを削除する前に、以下の項目が正常に動作することを確認してください。
  1. VM 内から ls data-handson-bucket-251226 を実行し、GCS 上のファイルがファイルシステムとして見えているか。
  2. gsutil lifecycle get gs://data-handson-bucket-251226 を実行し、NEARLINE への移行ルールが表示されるか。
  3. Spanner で JOIN クエリを実行し、期待通りの結果が返ってくるか。
  4. GCE のディスクスナップショットが作成されていることをコンソールまたは gcloud compute snapshots list で確認する。

ステップ 9:リソースの削除

課金を防ぐため、作成したすべてのリソースを削除します。
# Terraformによる一括削除
terraform destroy -auto-approve