Terraform書かずにインフラ管理してる奴、正気か? — マルチクラウドの正解構成を晒す

単一のクラウドプロバイダに依存するリスクを避けるため、マルチクラウド構成を検討する企業が増えている。しかし「マルチクラウドをやりたい」という声は多くても、実際にInfrastructure as Code(IaC)で管理している事例はまだ少ない。この記事では、TerraformでAWSとGCPのハイブリッド構成を管理する実践的な方法を、コード例を交えて解説する。

なぜマルチクラウドなのか

マルチクラウドを採用する動機はさまざまだが、主な理由は以下の通りだ。

  • ベンダーロックインの回避。単一プロバイダの障害時に事業継続できる体制を作る
  • 各クラウドの強みを活かす。機械学習はGCP、エンタープライズ統合はAWSなど
  • コスト最適化。ワークロードに応じて最もコスト効率の良いプロバイダを選択する
  • 規制対応。データの所在地や主権に関する要件を満たす

一方で、マルチクラウドには複雑性の増大、運用コストの上昇、チームのスキルセット拡大といった課題もある。Terraformはこれらの課題を緩和するための強力なツールだ。

Terraformプロジェクト構成

マルチクラウド環境でのTerraformプロジェクトは、モジュール設計が鍵になる。以下に推奨するディレクトリ構成を示す。

terraform/
├── environments/
│   ├── production/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── staging/
│       ├── main.tf
│       └── ...
├── modules/
│   ├── aws/
│   │   ├── vpc/
│   │   ├── eks/
│   │   ├── rds/
│   │   └── s3/
│   ├── gcp/
│   │   ├── vpc/
│   │   ├── gke/
│   │   ├── cloudsql/
│   │   └── gcs/
│   └── shared/
│       ├── dns/
│       ├── monitoring/
│       └── vpn/
└── global/
    ├── iam/
    └── state-backend/

この構成のポイントは、各クラウドのリソースをモジュールとして分離しつつ、共通のインフラ(VPN接続、DNS、モニタリング)をsharedモジュールで管理することだ。

プロバイダ設定

Terraformの強みは、複数のプロバイダを1つの設定ファイルで扱える点にある。以下は本番環境のプロバイダ設定例だ。

# environments/production/main.tf

terraform {
  required_version = ">= 1.7.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.40"
    }
    google = {
      source  = "hashicorp/google"
      version = "~> 5.20"
    }
    google-beta = {
      source  = "hashicorp/google-beta"
      version = "~> 5.20"
    }
  }

  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      Environment = "production"
      ManagedBy   = "terraform"
      Project     = "multicloud"
    }
  }
}

provider "google" {
  project = var.gcp_project_id
  region  = var.gcp_region
}

provider "google-beta" {
  project = var.gcp_project_id
  region  = var.gcp_region
}

AWS側: VPC + EKSの構築

AWS側ではVPCを作成し、EKSクラスタをデプロイする。本番環境ではマルチAZ構成にし、プライベートサブネットにワーカーノードを配置する。

# modules/aws/vpc/main.tf

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.project}-${var.environment}-vpc"
  }
}

resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 4, count.index)
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${var.project}-${var.environment}-private-${count.index}"
    "kubernetes.io/role/internal-elb" = "1"
  }
}

resource "aws_subnet" "public" {
  count                   = length(var.availability_zones)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 4, count.index + length(var.availability_zones))
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project}-${var.environment}-public-${count.index}"
    "kubernetes.io/role/elb" = "1"
  }
}

EKSクラスタモジュール

# modules/aws/eks/main.tf

resource "aws_eks_cluster" "main" {
  name     = "${var.project}-${var.environment}"
  role_arn = aws_iam_role.cluster.arn
  version  = var.kubernetes_version

  vpc_config {
    subnet_ids              = var.subnet_ids
    endpoint_private_access = true
    endpoint_public_access  = var.public_access
    security_group_ids      = [aws_security_group.cluster.id]
  }

  encryption_config {
    resources = ["secrets"]
    provider {
      key_arn = var.kms_key_arn
    }
  }

  enabled_cluster_log_types = [
    "api", "audit", "authenticator",
    "controllerManager", "scheduler"
  ]
}

resource "aws_eks_node_group" "main" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "${var.project}-${var.environment}-workers"
  node_role_arn   = aws_iam_role.node.arn
  subnet_ids      = var.subnet_ids

  scaling_config {
    desired_size = var.desired_size
    max_size     = var.max_size
    min_size     = var.min_size
  }

  instance_types = var.instance_types

  update_config {
    max_unavailable = 1
  }
}

GCP側: VPC + GKEの構築

GCP側でも同等の構成をTerraformで管理する。GKEはAutopilotモードを採用し、ノード管理の運用負荷を軽減する。

# modules/gcp/vpc/main.tf

resource "google_compute_network" "main" {
  name                    = "${var.project}-${var.environment}-vpc"
  auto_create_subnetworks = false
  routing_mode            = "GLOBAL"
}

resource "google_compute_subnetwork" "main" {
  name          = "${var.project}-${var.environment}-subnet"
  ip_cidr_range = var.subnet_cidr
  region        = var.region
  network       = google_compute_network.main.id

  secondary_ip_range {
    range_name    = "pods"
    ip_cidr_range = var.pods_cidr
  }

  secondary_ip_range {
    range_name    = "services"
    ip_cidr_range = var.services_cidr
  }

  private_ip_google_access = true
}

GKE Autopilotクラスタ

# modules/gcp/gke/main.tf

resource "google_container_cluster" "main" {
  provider = google-beta

  name     = "${var.project}-${var.environment}"
  location = var.region

  enable_autopilot = true

  network    = var.network_id
  subnetwork = var.subnetwork_id

  ip_allocation_policy {
    cluster_secondary_range_name  = "pods"
    services_secondary_range_name = "services"
  }

  private_cluster_config {
    enable_private_nodes    = true
    enable_private_endpoint = false
    master_ipv4_cidr_block  = var.master_cidr
  }

  release_channel {
    channel = "REGULAR"
  }

  workload_identity_config {
    workload_pool = "${var.gcp_project_id}.svc.id.goog"
  }

  binary_authorization {
    evaluation_mode = "PROJECT_SINGLETON_POLICY_ENFORCE"
  }
}

クラウド間VPN接続

マルチクラウドの要はクラウド間のネットワーク接続だ。AWSとGCPをVPNで接続し、プライベートネットワーク経由で通信できるようにする。

# modules/shared/vpn/main.tf

# GCP側: VPNゲートウェイ
resource "google_compute_ha_vpn_gateway" "to_aws" {
  name    = "vpn-to-aws"
  network = var.gcp_network_id
  region  = var.gcp_region
}

# GCP側: External VPN Gateway (AWS側のエンドポイント)
resource "google_compute_external_vpn_gateway" "aws" {
  name            = "aws-vpn-gateway"
  redundancy_type = "TWO_IPS_REDUNDANCY"

  interface {
    id         = 0
    ip_address = var.aws_vpn_tunnel1_address
  }

  interface {
    id         = 1
    ip_address = var.aws_vpn_tunnel2_address
  }
}

# GCP側: VPNトンネル
resource "google_compute_vpn_tunnel" "to_aws_1" {
  name                            = "vpn-to-aws-tunnel-1"
  vpn_gateway                     = google_compute_ha_vpn_gateway.to_aws.id
  peer_external_gateway           = google_compute_external_vpn_gateway.aws.id
  peer_external_gateway_interface = 0
  shared_secret                   = var.vpn_shared_secret
  router                          = google_compute_router.vpn.id
  vpn_gateway_interface           = 0
}

Stateの分割とリモート参照

マルチクラウド環境では、Terraform Stateの管理戦略が重要になる。一つの巨大なStateファイルですべてを管理するのは危険だ。以下の方針でStateを分割することを推奨する。

  • クラウドプロバイダごとにStateを分離する
  • ネットワーク、コンピュート、データベースなどのレイヤーごとにさらに分割する
  • terraform_remote_state データソースで他のStateの出力を参照する
  • Terraform CloudやSpaceliftなどのプラットフォームでState管理を一元化する
# GCP側からAWS VPNの情報を参照
data "terraform_remote_state" "aws_network" {
  backend = "s3"
  config = {
    bucket = "mycompany-terraform-state"
    key    = "production/aws/network/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

locals {
  aws_vpn_tunnel1_address = data.terraform_remote_state.aws_network.outputs.vpn_tunnel1_address
  aws_vpn_tunnel2_address = data.terraform_remote_state.aws_network.outputs.vpn_tunnel2_address
}

CI/CDとの統合

Terraformの変更をGitHub Actionsで自動化する。PRで plan を実行し、mainマージ時に apply する。

# .github/workflows/terraform.yml
name: Terraform

on:
  pull_request:
    paths: ['terraform/**']
  push:
    branches: [main]
    paths: ['terraform/**']

jobs:
  plan:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - name: Terraform Init
        run: terraform init
        working-directory: terraform/environments/production
      - name: Terraform Plan
        run: terraform plan -out=plan.tfplan
        working-directory: terraform/environments/production
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          GOOGLE_CREDENTIALS: ${{ secrets.GCP_SA_KEY }}

  apply:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - name: Terraform Apply
        run: |
          terraform init
          terraform apply -auto-approve
        working-directory: terraform/environments/production

コスト管理の自動化

マルチクラウド環境ではコスト管理が複雑になる。InfracostをCIパイプラインに組み込み、PR時点でコスト影響を可視化する。

# CIパイプラインでの実行例
$ infracost breakdown --path=terraform/environments/production

Project: terraform/environments/production

 Name                                    Monthly Qty  Unit  Monthly Cost
 ─────────────────────────────────────────────────────────────────────
 aws_eks_cluster.main
 └─ EKS cluster                               730  hours        $73.00

 aws_eks_node_group.main
 └─ 3 x m5.xlarge (on-demand)               2,190  hours       $630.72

 google_container_cluster.main
 └─ Autopilot (per vCPU hour)                 730  hours       $116.80
 └─ Autopilot (per GB hour)                   730  hours        $12.78

 OVERALL TOTAL                                                  $833.30

まとめ: マルチクラウドは手段であって目的ではない

Terraformによるマルチクラウド管理は強力だが、すべてのチームに必要というわけではない。マルチクラウドを採用する前に、以下の点を検討すべきだ。

  • 本当にマルチクラウドが必要なのか、単一クラウドのマルチリージョンで十分ではないか
  • チームにマルチクラウドを管理するスキルセットがあるか
  • 運用コストの増大を許容できるか
  • 各クラウドのマネージドサービスを使い分けるのか、それともクラウドアグノスティックな構成にするのか

マルチクラウドは銀の弾丸ではない。しかし、適切に設計・運用すれば、ビジネスの回復力とコスト効率を大幅に向上させることができる。Terraformはその実現を支える最も実用的なツールの一つだ。

次回は、この構成にObservabilityレイヤー(Prometheus, Grafana, OpenTelemetry)を追加する方法を解説する予定だ。