ぶていのログでぶログ

思い出したが吉日

Terraformでfor文とfileset関数を使って複数の設定ファイルの読み込みをDRYに書く

Terraformにおいて例えば、A,B,Cという種類のサーバの属性がありそれぞれモジュールで定義されていて、それらに属するサーバが複数あると行った場合、何も考えずに書くと以下のようなコードになると思う。

locals {
  servers_a = {
    node-a-1 = "192.168.0.1",
  }

  servers_b = {
    node-b-1 = "192.168.1.1",
    node-b-2 = "192.168.1.2",
  }

  servers_c = {
    node-c-1 = "192.168.2.1",
    node-c-2 = "192.168.2.2",
    node-c-3 = "192.168.2.3",
  }
}

module "server_a" {
  source    = "module/server_a"
  for_each  = local.servers_a
  name      = each.key
  ipaddress = each.value
}

module "server_b" {
  source    = "module/server_b"
  for_each  = local.servers_b
  name      = each.key
  ipaddress = each.value
}

module "server_c" {
  source    = "module/server_c"
  for_each  = local.servers_c
  name      = each.key
  ipaddress = each.value
}

ここでWorkspaceを考慮する必要が出てきた場合、読み替えが必要になり以下のように分岐を入れることになる。 以下の例ではproductionとそれ以外で分岐を入れている。

locals {
  servers_a = terraform.workspace == 'production' ? {
    node-a-1 = "192.168.0.1",
  } : {
    test-node-a-1 = "192.168.10.1",
  }

  servers_b = terraform.workspace == 'production' ? {
    node-b-1 = "192.168.1.1",
    node-b-2 = "192.168.1.2",
  } : {
    test-node-b-1 = "192.168.11.1",
  }

  servers_c = terraform.workspace == 'production' ? {
    node-c-1 = "192.168.2.1",
    node-c-2 = "192.168.2.2",
    node-c-3 = "192.168.2.3",
  } : {}
}

# 以下moduleの定義は同じなので省略 #

分岐が入りかなり見通しが悪いコードになってしまった…。 ここにさらにWorkspaceが増えたり、サーバの種類やノードが増えてしまうとズラズラとファイルが長くなっていきつらい感じになってくる…。 module単位で別のファイルに切り出すという方法もあるが、localsの定義で分岐をしているのでここをDRYに書きたくなってくる。

Terraformではyamldecode/jsondecodeという関数が用意されていて、これを使うとlocalsをスッキリ書くことができる。 以下は environtment/<worspace>/<サーバの種類>.yamlなファイルを読み込むように変えたコードである。

locals {
  servers_a = yamldecode(file(format("./environment/%s/servers_a.yaml", terraform.workspace)))
  servers_b = yamldecode(file(format("./environment/%s/servers_b.yaml", terraform.workspace)))
  servers_c = yamldecode(file(format("./environment/%s/servers_c.yaml", terraform.workspace)))
}

# 以下moduleの定義は同じなので省略 #

YAMLファイルは以下のような感じ

# ./environment/production/servers_a.yaml
node-a-1: 192.168.0.1

# ./environment/production/servers_b.yaml
node-b-1: 192.168.1.1
node-b-2: 192.168.1.2

# ./environment/production/servers_c.yaml
node-c-1: 192.168.2.1
node-c-2: 192.168.2.2
node-c-3: 192.168.2.3

# ./environment/default/servers_a.yaml
test-node-a-1: 192.168.10.1

# ./environment/default/servers_b.yaml
test-node-b-1: 192.168.11.1

# ./environment/default/servers_c.yaml
# yamldecodeやfor_eachがエラーになるので空のハッシュを定義しておく
{}

YAMLファイルにサーバの情報を外だしすることでコードがスッキリしたと思う。 しかしながら、モジュールごとに変数を定義しないといけなかったり、./environment/default/servers_c.yamlのように空のハッシュを定義するファイルを設置しなくてはならなくてめんどくさい…。 もう少しいい感じにできないかと悩んでいたところ、以下のサイトにたどりつきfileset関数とfor文を使う方法を知った*1

discuss.hashicorp.com

これらを使うと以下のように書ける。

locals {
  servers = {
    for f in fileset("", format("./environment/%s/*.yaml", terraform.workspace)):
      replace(basename(f), "/\\.yaml/", "") => yamldecode(file(f))
  }
}

module "server_a" {
  source    = "module/server_a"
  for_each  = try(local.servers["server_a"], {})
  name      = each.key
  ipaddress = each.value
}

module "server_b" {
  source    = "module/server_b"
  for_each  = try(local.servers["server_b"], {})
  name      = each.key
  ipaddress = each.value
}

module "server_c" {
  source    = "module/server_c"
  for_each  = try(local.servers["server_c"], {})
  name      = each.key
  ipaddress = each.value
}

YAMLファイルは前項と同じなので省略。 ./environment/default/servers_c.yaml を削除してもエラーにはならないはず。 最初のコードに比べるとかなりスッキリしたし、コードとデータを分離できて管理しやすくなったと思う。 最高〜

*1:この記事でやりかったこととは問題点が違うがfileset関数とfor文を使うというアイディアが参考になった