kawabatas技術ブログ

試してみたことを書いていきます

Terraform で GCP のリソースを管理する

Terraform で GCP のリソースを管理する

概要

GKE, Cloud SQL, Cloud Storage, Cloud Memorystore など、gcloud コマンドで構築していたが、 今後、本番、負荷試験、開発環境を増やす、といった際に、すぐに構築できるように Terraform の導入を検討してみた。

Terraform

Install

Install

自分は homebrew で入れた

$ brew install terraform
$ terraform -v
Terraform v0.11.7

使う

方針

  • terraform 用のサービスアカウントは作成せず、owner 権限を持った個人が terraform コマンドを実行する。
  • .tfstate は GCS で管理する。
  • workspace を切り替えて、環境ごとに操作する。
  • 今回は Cloud SQL, Cloud Storage, Cloud Memorystore のコードをのせ、GKE については別でまとめたい。 ※GKE は同じ cluster に namespace で環境を用意しようと思っているが、負荷試験環境では新しく cluster を作らればならないと考えている。cluster のスペックの検証のために。

terraform ディレクトリを作成し、そこでリソース操作を行う。

$ mkdir terraform
$ cd terraform

フォルダ構成をどうするか

構成1

公式で推奨している構成で、

  • tarraform のルートディレクトリで workspace を操作し、
  • tarraform のルートディレクトリで terraform init <サブディレクトリ> で state ファイルを gcs で管理すると、
  • tarraform のルートディレクトリで workspace を切り替えられないエラーに直面した。

具体的にいうと、下記のフォルダ構成において

$ tree
.
├── environments
│   ├── production
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── provider.tf
│   │   └── variables.tf
│   └── staging
│       ├── backend.tf
│       ├── main.tf
│       ├── provider.tf
│       └── variables.tf
├── modules
│   ├── db
│   │   ├── main.tf
│   │   └── variables.tf
│   ├── redis
│   │   ├── main.tf
│   │   └── variables.tf
│   └── storage
│       ├── main.tf
│       └── variables.tf

tarraform のルートディレクトリで workspace を操作

$ pwd
<ディレクトリ>/terraform
$ terraform workspace show
staging

tarraform のルートディレクトリでサブディレクトリを init

$ terraform init environments/staging/
略
Terraform has been successfully initialized!

workspace 切り替えるとエラー

$ terraform workspace select production
Backend reinitialization required. Please run "terraform init".
Reason: Unsetting the previously set backend "gcs"

The "backend" is the interface that Terraform uses to store state,
perform operations, etc. If this message is showing up, it means that the
Terraform configuration you're using is using a custom configuration for
the Terraform backend.

Changes to backend configurations require reinitialization. This allows
Terraform to setup the new configuration, copy existing state, etc. This is
only done during "terraform init". Please run that command now then try again.

If the change reason above is incorrect, please verify your configuration
hasn't changed and try again. At this point, no changes to your existing
configuration or state have been made.

Failed to load backend: Initialization required. Please see the error message above.

同じ内容の issue を見つけた。

For now the recommended workflow is to always run terraform from the root of the configuration for consistency, which also has the benefit of making it explicit which configuration the working directory has been initialized for.

どうやら設定ファイル(.tf)のルートディレクトリで terraform コマンドを実行することを想定していて、terraform init <サブディレクトリ>は考慮されていないらしい...

公式で推奨している構成と矛盾している気がするが、仕方ない。。。

構成2
├── cluster(GKE用)
│   ├── backend.tf
│   ├── main.tf
│   ├── provider.tf
│   └── variables.tf
├── datastores
│   ├── backend.tf
│   ├── main.tf
│   ├── modules
│   │   ├── db
│   │   │   ├── main.tf
│   │   │   └── variables.tf
│   │   ├── redis
│   │   │   ├── main.tf
│   │   │   └── variables.tf
│   │   └── storage
│   │       ├── main.tf
│   │       └── variables.tf
│   ├── provider.tf
│   └── variables.tf

このようなフォルダ構成にし、datastores ディレクトリで workspace (staging, production, stress) を作成し、cloud_sql, cloud_memorystore, cloud_storage リソースを操作することにする。

terraform/resource ディレクトリで、 workspace を切り替えて操作する。

$ cd terraform/<resource>

workspace を切り替え

$ terraform workspace list
$ terraform workspace select staging

init。module を更新した際も初期化が必要

$ terraform init

リソース作成の計画を表示する

$ terraform plan

リソース作成を行う

$ terraform apply

コードフォーマットを整える

$ terraform fmt

.tfstate を表示する

$ terraform state pull

コード

コードもどんな感じか載せておく。

GCP のドキュメント

datastores/backend.tf

terraform {
  backend "gcs" {
    bucket = "<バケット名>"
    prefix = "datastores"
  }
}

data "terraform_remote_state" "backend" {
  backend   = "gcs"
  workspace = "${terraform.workspace}"

  config {
    bucket = "<バケット名>"
    prefix = "datastores"
  }
}

datastores/main.tf

module "db" {
  source = "./modules/db"
  common = "${var.common}"
  db     = "${var.db}"
}

datastores/provider.tf

provider "google" {
  project = "<GCPプロジェクト名>"
}

datastores/variables.tf

variable "common" {
  default = {
    default.name   = "development"
    default.region = "asia-northeast1"

    # workspace ごとの設定
    staging.name = "staging"
  }
}

variable "db" {
  default = {
    default.tier = "db-n1-standard-1"

    # workspace ごとの設定
    staging.tier = "db-n1-standard-1"
  }
}

datastores/modules/db/main.tf

resource "google_sql_database_instance" "db" {
  name             = "${lookup(var.common, "${terraform.workspace}.name", var.common["default.name"])}"
  database_version = "MYSQL_5_7"
  region           = "${lookup(var.common, "${terraform.workspace}.region", var.common["default.region"])}"

  settings {
    tier             = "${lookup(var.db, "${terraform.workspace}.tier", var.db["default.tier"])}"
    replication_type = "SYNCHRONOUS"

    database_flags {
      name  = "character_set_server"
      value = "utf8mb4"
    }

    database_flags {
      name  = "slow_query_log"
      value = "on"
    }

    database_flags {
      name  = "long_query_time"
      value = "0.2"
    }

    database_flags {
      name  = "lock_wait_timeout"
      value = "1"
    }
  }
}

resource "google_sql_database" "database" {
  name     = "<データベース名>"
  instance = "${google_sql_database_instance.db.name}"
}

resource "google_sql_user" "user" {
  name     = "<ユーザ名>"
  instance = "${google_sql_database_instance.db.name}"
  host     = "cloudsqlproxy~%"
}

resource "google_service_account" "cloud_sql_proxy_service_account" {
  account_id   = "${lookup(var.common, "${terraform.workspace}.name", var.common["default.name"])}-cloud-sql-proxy"
  display_name = "${lookup(var.common, "${terraform.workspace}.name", var.common["default.name"])} Cloud SQL Proxy"
}

resource "google_project_iam_member" "cloud_sql_proxy_service_account" {
  role   = "roles/cloudsql.client"
  member = "serviceAccount:${google_service_account.cloud_sql_proxy_service_account.email}"
}

datastores/modules/db/variables.tf

variable "common" {
  default = {}
}

variable "db" {
  default = {}
}