🌤️

Terraform + Cloud SQL + Cloud Run

2023/03/19に公開1

概要

さくっとwebサーバーを立てたいとき、この構成が一番速いと思うのでTerraformの構成を共有します。

フォルダ構成

以下のようなフォルダ構成をとっています

プロジェクトID/
 ├ docker-compose.yml ← ローカル開発用
 ├ backend/ ← アプリ
 │ ├ Dockerfile
 │ └ app
 ├ mysql/ ← DB用
 │ └ my.cnf
 └ terraform/ ← クラウドクラウドデプロイ用
   ├ main.tf
   └ secret.tfvars

アプリ

アプリはRubyでもGoでもRustでも構わないので、PORT環境変数のポートで待ち受けるようにしてdocker buildします。

ローカルで動作が確認できたら、以下のようにGoogle Container Registryにプッシュします。
このコンテナがCloud Run上で動作します。

cd backend
docker build -t backend:latest .
docker tag backend:latest asia.gcr.io/(プロジェクトID)/backend:latest

docker push asia.gcr.io/(プロジェクトID)/backend:latest

GCPのAPIの有効化

以下のAPIを有効にします。

  • Cloud Run Admin API
  • Cloud DNS API
  • Compute Engine API
  • Cloud Domains API
  • Cloud SQL Admin API
  • Network Management API

terraform

以下の2ファイルを同じフォルダに置いて、以下のコマンドを実行すれば環境が作られます。

cd terraform
terraform init
terraform plan -var-file="secret.tfvars"
terraform apply -var-file="secret.tfvars"
  • secret.tfvars(.gitignoreすること)
project_id = "(プロジェクトID)sampla-app"
project_id_underscore = "(アンダースコア区切りのプロジェクトID )sampla_app"
location = "asia-northeast1"
zone = "asia-northeast-b"
domain_root = "(Cloud Domainsで取得したドメイン)"

gcr_app_server_tag = "(gcrにプッシュしたコンテナのタグ)"
app_db_user_password = "xxxx"
  • main.tf
provider "google" {
  project = var.project_id
  region  = var.location
  zone    = var.zone
}

#
# Variables
#
variable "project_id" {
  type = string
}

variable "project_id_underscore" {
  type = string
}

variable "location" {
  type = string
}

variable "zone" {
  type = string
}

variable "domain_root" {
  type = string
}

variable "gcr_app_server_tag" {
  type = string
}

variable "app_db_user_password" {
  type = string
}

#
# Cloud SQL
#
resource "google_sql_database_instance" "master_instance" {
  name             = "${var.project_id}-instance"
  database_version = "MYSQL_8_0_31"
  settings {
    tier = "db-f1-micro"
  }

  deletion_protection  = "true"
}

resource "google_sql_database" "main_database" {
  name      = "${var.project_id_underscore}_database"
  instance  = google_sql_database_instance.master_instance.name
  charset   = "utf8mb4"
  collation = "utf8mb4_bin"
}

resource "google_sql_user" "app_db_user" {
  name     = "${var.project_id_underscore}_db_user"
  instance = google_sql_database_instance.master_instance.name
  host     = "%"
  password = var.app_db_user_password
}

#
# Cloud Run
#
resource "google_cloud_run_service" "app_server" {
  name     = "${var.project_id}-service"
  location = var.location

  template {
    spec {
      containers {
        image = var.gcr_app_server_tag
        env {
          name  = "DATABASE_AUTO_MIGRATION"
          value = "true"
        }
        env {
          name  = "DATABASE_PROTOCOL"
          value = "cloudsql"
        }
        env {
          name  = "DATABASE_HOST"
          value = google_sql_database_instance.master_instance.connection_name
        }
        env {
          name  = "DATABASE_TIMEZONE"
          value = "UTC"
        }
        env {
          name  = "DATABASE_NAME"
          value = google_sql_database.main_database.name
        }
        env {
          name  = "DATABASE_USER"
          value = google_sql_user.app_db_user.name
        }
        env {
          name  = "DATABASE_PASSWORD"
          value = var.app_db_user_password
        }
      }
    }

    metadata {
      annotations = {
        "autoscaling.knative.dev/maxScale"      = "1000"
        "run.googleapis.com/cloudsql-instances" = google_sql_database_instance.master_instance.connection_name
        "run.googleapis.com/client-name"        = "terraform"
      }
    }
  }

  metadata {
    annotations = {
      "run.googleapis.com/launch-stage" = "BETA"
    }
  }
}

data "google_iam_policy" "noauth" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "noauth" {
  location    = google_cloud_run_service.app_server.location
  project     = google_cloud_run_service.app_server.project
  service     = google_cloud_run_service.app_server.name

  policy_data = data.google_iam_policy.noauth.policy_data
}

// ------------------------------------
// Cloud DNSの設定
// カスタムドメインを使わない場合は以降不要
// ------------------------------------
resource "google_dns_managed_zone" "app_server_dns_zone" {
  name        = "app-server-zone"
  dns_name    = "${var.domain_root}."
  description = "DNS zone (${var.domain_root})"
  dnssec_config {
    state = "on"
  }
}

resource "google_cloud_run_domain_mapping" "domain_mapping" {
  location = var.location
  name     = "${var.project_id}.${var.domain_root}"

  metadata {
    namespace = var.project_id
  }

  spec {
    route_name = google_cloud_run_service.app_server.name
  }
}

resource "google_dns_record_set" "app_server_domain" {
  managed_zone = google_dns_managed_zone.app_server_dns_zone.name
  name = "${var.project_id}.${google_dns_managed_zone.app_server_dns_zone.dns_name}"
  type = "CNAME"
  ttl  = 300

  rrdatas = ["ghs.googlehosted.com."]
}

resource "google_compute_managed_ssl_certificate" "app_server_cert" {
  provider = google
  name     = "${var.project_id}-ssl-cert"

  managed {
    domains = ["${var.project_id}.${var.domain_root}"]
  }
}

CNAMEが反映されるまで30分ほどかかりますが、反映されればSSLを使ってカスタムドメイン経由でCloud Runに接続できるようになります。

MySQL用

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[client]
default-character-set=utf8mb4

docker-compose.yml

こちらはローカル開発用です。

version: '3.7'
services:
  server:
    depends_on:
      - mysql
    image: backend/latest
    build:
      context: ./backend
    environment:
      - TZ=UTC
      - HOST=0.0.0.0
      - DATABASE_AUTO_MIGRATION=true
      - DATABASE_HOST=mysql
      - DATABASE_NAME=xxxx
      - DATABASE_TIMEZONE=UTC
      - DATABASE_USER=user
      - DATABASE_PASSWORD=password
    ports:
      - "8080:8080"
  mysql:
    image: mysql:latest
    volumes:
      - ./mysql/data:/var/lib/mysql
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./mysql/init:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: xxxx
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      TZ: UTC
Happy Elements

Discussion

Kazuki HASEGAWAKazuki HASEGAWA

Cloud Runにカスタムドメインをマップする場合、対象のドメインをGoogle Domainsで管理していると、Cloud DNSを使わなくても正しく機能するようになります
※どちらもネームサーバーがghs.googlehosted.com.のため

Terraformとの相性考えるとCloud DNSが便利なんですけどね。