はじめに
こんにちは!masa-asaです。
突然ですが、以前このような記事を投稿しました。
この記事は1つの環境にインフラを構築することを想定していますが、dev/stg/prd環境など複数の環境に合わせて名前などを変えたい場合どのような構成にするべきか気になったので、ここにまとめようと思います。
また、その他のベストプラクティスについても、よく使いそうな部分を抜粋して述べようと思います。
今回の内容は主にterraformのスタイルガイドを参考に記述しました。
複数環境を持つ際のフォルダ構成
以下のような構成で実現できそうです。これはスタイルガイドの「Multiple environments」を参考にしています。
├── modules
│ ├── cloud_run
│ │ └── main.tf
│ ├── cloud_storage
│ │ └── main.tf
│ └── variables.tf
├── dev
│ ├── backend.tf
│ ├── main.tf
│ └── variables.tf
│ └── terraform.tfvars
├── prd
│ ├── backend.tf
│ ├── main.tf
│ └── variables.tf
└── stg
├── backend.tf
├── main.tf
└── variables.tf
devフォルダについて
構成についてそれぞれの働きを見ていきます。devフォルダの「main.tf」を見てみます。
「main.tf」にはこの環境で実際に作成するリソースが記述されています。ここではmodulesフォルダで定義したリソースをインポートしてその環境(dev)で作成する定義です。
「main.tf」ではenv = var.envという記述をしています。これは、このモジュールに対して「env」という変数を設定するという内容です。
module "cloud_storage" {
source = "../modules/cloud_storage"
env = var.env
}
module "cloud_run" {
source = "../modules/cloud_run"
env = var.env
}
devフォルダ内の「variables.tf」には以下のような記述がされています。ここでは「env」という変数を宣言しています。ここで変数宣言することで、「main.tf」でもその変数が使えるようになります。
variable "env" {
description = "Environment name"
type = string
}
「terraform.tfvars」では、具体的な変数の値を設定しています。例のように「env」という変数に「dev」という具体的な値を設定しています。
env = "dev"
modulesフォルダについて
modulesフォルダの中にあるcloud_runの「main.tf」を見てみます。
name = "crun-sandbox-${var.env}" というように、「env」という変数を設定しています。
resource "google_cloud_run_v2_service" "default" {
name = "crun-sandbox-${var.env}"
location = "asia-northeast1"
deletion_protection = false
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
template {
max_instance_request_concurrency = 20
scaling {
max_instance_count = 1
}
containers {
image = "us-docker.pkg.dev/cloudrun/container/hello"
resources {
limits = {
cpu = "1"
memory = "512Mi"
}
}
}
}
}
cloud_runの下にある「variables.tf」でも「env」という変数を宣言しています。
これは、cloud runフォルダの中で有効なenvという変数の宣言です。
variable "env" {
description = "Environment name"
type = string
}
変数の動作
上記の構成のように、devフォルダとmoduleフォルダそれぞれで変数の宣言をしています。
この際、実際にリソースを作る場合はdevフォルダ内にある「main.tf」に対してterraform planやterraform applyコマンドを実行します。
「main.tf」はモジュールとしてmoduleフォルダ内で定義した各リソースを読み込みます。sourceでは読み込むフォルダのパス(ここでは相対パス)を示しています。
「env」は、このモジュールで利用する変数です。これは自身で決めた変数を記述します(例えば、「name」や「location」などなど、自身で宣言した変数名をここで用います)。
「env」という変数の値はvar.envから取得されます。これは、devフォルダ内の「variables.tf」で宣言された変数を示しており、その具体的な値は「terraform.tfvars」で記述された値です。
module "cloud_storage" {
source = "../modules/cloud_storage"
env = var.env
}
module "cloud_run" {
source = "../modules/cloud_run"
env = var.env
}
実際にこのterraformが実行されると、記述されたモジュールのパスが読み込まれ、そのフォルダ内のterraformファイルが実行されます。
cloud_runフォルダを例にみると、リソースの定義が「main.tf」で行われおり、そこで使う変数が「variables.tf」で宣言されています。devフォルダの「main.tf」でcloud_runのモジュールが呼ばれると、そこで使う変数がcloud_run側にも渡されて利用されるというイメージです。
そのため、devなどの環境側とモジュール側それぞれで利用する変数を宣言する必要があります。
そのほかの書き方ベストプラクティス(一部)
リソース
リソース名「名詞」を用いる必要があり、リソース名にリソースタイプを用いるべきではない
良い例だとリソース名が「frontend_lb_ip」で悪い例だと「global_address_lb_ip」です。良い例だとこのリソースがどんな役割を持つのか明確でわかりやすい一方、悪い例だと、リソースタイプとリソース名が重複し、このリソースの役割が一目でわかりにくいです。
良い例
resource "google_compute_global_address" "frontend_lb_ip" {
address_type = "EXTERNAL"
description = "フロントエンドのCloud Runに対するLBに付与する静的IP"
name = "lb_static_ip_sandbox"
}
悪い例
resource "google_compute_global_address" "global_address_lb_ip" {
address_type = "EXTERNAL"
description = "フロントエンドのCloud Runに対するLBに付与する静的IP"
name = "lb_static_ip_sandbox"
}
これらの書き方の違いは、リソースの値をほかのリソースで参照する際の記述に着目するとよくわかります。
この例は上記のリソースのidを示すコードですが、悪い例だと「global_address」という文字が連続し長くなってしまううえにこのリソースがどんな役割のものかわかりにくくなってしまっています。
良い例
google_compute_global_address.frontend_lb_ip.id
悪い例
google_compute_global_address.global_address_lb_ip.id
単語はアンダースコア(_)で区切る・リソースタイプとリソース名はダブルクオーテーション(")で囲う
リソース名の例「frontend_lb_ip」のように単語はアンダースコアで区切ります(スネークケース)。
また「"google_compute_global_address"」や「"frontend_lb_ip"」のようにリソースタイプとリソース名はダブルクオーテーションで囲むように記述します。
resource "google_compute_global_address" "frontend_lb_ip" {
address_type = "EXTERNAL"
description = "フロントエンドのCloud Runに対するLBに付与する静的IP"
name = "lb_static_ip_sandbox"
}
参照するリソースの後に、依存リソースを定義する
複数のリソースがあり、それぞれに依存関係がある場合、初めに「依存される側」のリソースを記述し、後に依存するリソースを書くようにします。
例ではVPCが何にも依存していないリソースであり、これを先に書いています。
サブネットとファイアウォールはそれぞれVPCに依存するので、VPCのリソースの定義の後に記述しています。
# VPCネットワークの定義
resource "google_compute_network" "example" {
name = "example-network"
}
# サブネットの定義(VPCネットワークに依存)
resource "google_compute_subnetwork" "example" {
name = "example-subnetwork"
network = google_compute_network.example.name
ip_cidr_range = "10.0.1.0/24"
region = "us-central1"
}
# ファイアウォールルールの定義(VPCネットワークに依存)
resource "google_compute_firewall" "example" {
name = "example-firewall"
network = google_compute_network.example.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"]
}
リソースの順序
リソース記述の順序は以下のように記述するようにします。
- 存在する場合、
countとfor_eachのメタ引数を記述 - リソース固有の非ブロックパラメータ
- リソース固有のブロックパラメータ
- ライフサイクルブロック
-
depends_onパラメータ
resource "google_compute_instance" "example" {
count = 2 # 複数のインスタンスを作成
name = "example-instance-${count.index}"
machine_type = "e2-medium"
zone = "us-central1-a"
# 非ブロックパラメータ(例:インスタンスのマシンタイプとゾーン)
boot_disk {
initialize_params {
image = "projects/debian-cloud/global/images/family/debian-10"
}
}
# ブロックパラメータ(例:ネットワークインターフェース設定)
network_interface {
subnetwork = google_compute_subnetwork.example.name
access_config {
# 外部IPを設定
}
}
# lifecycleブロック(必要に応じて使用)
lifecycle {
create_before_destroy = true
}
# depends_onパラメータ(必要に応じて使用)
depends_on = [google_compute_firewall.example]
}
変数(Variables)
変数の記述については、以下のような順序の構造で記述されることが推奨されます。以下でそれぞれについて説明します。
- タイプ(Type)
- 説明(Description)
- デフォルト値(Default)(オプション)
- センシティブ設定(Sensitive) (オプション)
- バリデーション設定(Validation blocks)
変数宣言には、タイプと説明を含む
例のように、変数宣言にはタイプ(type)と説明(description)を含めます。
variable "web_instance_count" {
type = number
description = "Number of web instances to deploy. This application requires at least two instances."
}
必要に応じてデフォルト値を設定する
defaultを設定することで、変数のデフォルト値を定義可能です。
variable "web_instance_count" {
type = number
description = "Number of web instances to deploy. This application requires at least two instances."
default = 2
}
センシティブな値を扱う際にはsensitive属性をtrueにする
sensitive属性をtrueにするとその変数の値は、terraform planやterraform applyに表示されなくなります。ただしterraformファイルには平文で値が記述されていることには注意が必要です。
値を秘匿した管理は別途「secrets management」という機能を使う必要があります。
variable "web_instance_count" {
type = number
description = "Number of web instances to deploy. This application requires at least two instances."
default = 2
sensitive = true
}
入力バリデーションを使う
validation句を用いることで、入力値のバリデーションチェックを行うことができます。condition句でバリデーションの具体的な条件を記述し、error_message句で違反時のエラーメッセージを記述します。
variable "web_instance_count" {
type = number
description = "Number of web instances to deploy. This application requires at least two instances."
default = 2
sensitive = true
validation {
condition = var.web_instance_count >= 1
error_message = "The number of web instances must be at least 1."
}
}
アウトプット(Output)
アウトプットは定義したリソースの情報をterraformのほかの部分で参照するように公開する機能です。
アウトプットは以下のような順序の構造で記述されることが推奨されます。
- 説明(Description)
- 値(Value)
- センシティブ設定(Sensitive) (オプション)
アウトプットには説明と値を含む
description句で対象のアウトプットの説明を記述し、value句で具体的なアウトプットの値を記述します。
resource "google_compute_global_address" "frontend_lb_ip" {
address_type = "EXTERNAL"
description = "フロントエンドのCloud Runに対するLBに付与する静的IP"
name = "lb_static_ip_sandbox"
}
output "frontend_lb_ip" {
description = "value of the global address"
value = google_compute_global_address.frontend_lb_ip.id
}
センシティブな値を扱う際にはsensitive属性をtrueにする
sensitive属性をtrueにするとその変数の値は、terraform planやterraform applyに表示されなくなります。
resource "google_compute_global_address" "frontend_lb_ip" {
address_type = "EXTERNAL"
description = "フロントエンドのCloud Runに対するLBに付与する静的IP"
name = "lb_static_ip_sandbox"
}
output "frontend_lb_ip" {
description = "value of the global address"
value = google_compute_global_address.frontend_lb_ip.id
sensitive = true
}
ローカル値(Local values)
ローカル値は「main.tf」などのファイル内で共通する値などを持ち再利用するためのローカル変数のような働きをします。設定したローカル値は${local.location}のような記述で利用できます。
locals {
location = "asia-northeast1"
}
resource "google_artifact_registry_repository" "docker" {
location = "${local.location}"
repository_id = "ar-repo-poc"
description = "example docker repository"
format = "DOCKER"
docker_config {
immutable_tags = true
}
}
また、ローカル値は変数を用いることもできます。例としては以下のようになります。
locals {
name_suffix = "${var.project_id}_${var.env}"
}
複数のファイルからローカル値を参照する場合は、「locals.tf」という名前のファイルにローカル値をまとめることが推奨されます。
リソースなど同一のファイルにローカル値を書く場合は、ローカル値を一番先頭で書くようにします。
for_eachとcount
for_each
for_eachを用いることで、リスト型やマップ型の変数に対して繰り返し処理を行うことができます。
リソースの定義の初めにfor_eachの値として、マップ型の変数を指定しています。マップの場合はfor_eachに直接、変数を指定できます。
マップ型の変数では、キー/値のペアによる表現がなされており、for_eachを用いたリソースの中でそれぞれeach.keyとeach.valueを指定することができます。
この例ではkeyが左辺(repo1やrepo2)に相当し、valueが右辺(Example Docker repository 1 ...)に当たります。
このterraformを実行すると、マップの要素数の回数リソースが作成され、結果的にArtifact Repositoryが4つできます。
# マップ型変数の`for_each`例
variable "artifact_repositories" {
type = map(string)
default = {
"repo1" = "Example Docker repository 1"
"repo2" = "Example Docker repository 2"
"repo3" = "Example Docker repository 3"
"repo4" = "Example Docker repository 4"
}
}
resource "google_artifact_registry_repository" "docker_repos" {
for_each = var.artifact_repositories
location = "asia-northeast1"
repository_id = each.key
description = each.value
format = "DOCKER"
docker_config {
immutable_tags = true
}
}
output "repository_descriptions" {
value = { for repo in google_artifact_registry_repository.docker_repos : repo.repository_id => repo.description }
}
output "repo1_description" {
value = google_artifact_registry_repository.docker_repos["repo1"].description
}
リスト型の場合、for_eachに渡す値は「toset()」でリスト型をセット型に変換する必要があります。
セットは重複を削除したユニークな値のみを許す型です。
セット型の場合each.keyとeach.valueの結果はどちらも要素の値という同じ結果を返します。
variable "repository_names" {
type = list(string)
default = ["repo1", "repo2", "repo3", "repo4"]
}
resource "google_artifact_registry_repository" "docker_repos" {
for_each = toset(var.repository_names)
location = "asia-northeast1"
repository_id = each.value # リストの各要素
description = "Example Docker repository for ${each.value}"
format = "DOCKER"
docker_config {
immutable_tags = true
}
}
count
countを使うことで、例のように指定した数だけリソースを作ることが可能になります。number型の変数を用いる場合、countの引数には直接変数を指定することができます。あるいは、count = 3のように数字を指定することも可能です。
${count.index}には繰り返しのインデックス番号が格納されます。0からカウントアップしていく形式です。
variable "instance_count" {
type = number
default = 3
}
resource "google_compute_instance" "example" {
count = var.instance_count
name = "example-instance-${count.index}"
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-9"
}
}
network_interface {
network = "default"
access_config {
// エフェメラルIPを設定
}
}
}
リスト型の変数を用いる場合は、countの引数でlength()関数を用いてリストの長さを渡してあげる必要があります。
variable "instance_names" {
type = list(string)
default = ["instance-1", "instance-2", "instance-3"]
}
resource "google_compute_instance" "example" {
count = length(var.instance_names) # リストの長さを指定
name = var.instance_names[count.index] # リストからインスタンス名を取得
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-9"
}
}
network_interface {
network = "default"
access_config {
// エフェメラルIPを設定
}
}
}
終わりに
IaCを実現するためにterraformを書くことが重要になります。今までなんとなくで書いていましたが、環境別に分けて、可読性・保守性の高いコードを書く場合にはしっかりとしたベースの知識が必要になると実感します。
terraformのスタイルガイドは分量が多く、ここですべてを網羅することはできていませんが、コードを書く上で何度も参照することになる重要なものと感じたので、今回書ききれなかった部分もいつか述べることができればと思います。
スタイルガイドのほかにも、公式サイトにはそれぞれの機能の詳細なドキュメントがあるので、それを適宜読み込むことも必要になりそうです!