shibatch's journey

日々考えていることをつらつら書くだけです

メンテナンスしやすいTerraformコードを書くために気をつけていること

TerraformというIaCのためのツールがあります。割とディレクトリ構成から管理の仕方までいろいろと自由がきくツールであります。

最近これでAWSVPCやEKS、RDS、ElastiCacheをコードにしたのだけれど、個人的に今まで他の方が書いたコードを読み解いたりもしてきた経験から、なんとなく「こう書いた方が他人には優しいだろう」「将来的なメンテナンスコストが抑えられるだろう」と思いつつ書いていることに気づいたので、自分がどういうことに気を付けているかを書き留めてみます。   …といってもそんなに奇をてらったようなテクニックを使っているわけではないので(むしろそういうことをするとメンテナンスしにくくなるので🙅‍♀️)そんなの常識じゃん、みたいなこともありそうだけれどとりあえず書いてみます。

1. module化は避ける

いきなり好みが分かれそうですが、自分がTerraformでコードを書くときはmodule化しません。module化は便利ですがTerraformのコードの階層が深くなりがちで、その分何が生成されるのかが他人から見たときに直感的に把握しにくくなると感じています。共通化はworkspaceの使用、for_eachやforを使った利用に極力とどめておくべきです。

2.tfstateはなるべく小さく

tfstateを小さくすることは何個かメリットがあります。ひとつはterraform plan / applyの実行時間が短くなること。もうひとつは複数人で作業していても更新した箇所とは無関係な箇所でterraformの差分が現れてこれ何だろう〜で時間を食うようなこと(これが本当によくある…)が防げます。 どの程度の範囲で単位にするのが良さそうかというと例えばRDSだとそのRDSを使うためのIAMやSGまで、VPCだとSubnetとInternetGatewayとNatGatewayくらいまでくらいが妥当じゃないでしょうか。

3.workspace、for_eachの積極活用

環境を分けるときはworkspaceを使う、複数台作成するときはfor_eachを使う。これで大体の場合は対応できるというか、逆にこの方法でコードにできないような構成を避ける、環境の方をTerraformで管理しやすい方に寄せるまであります。

RDS(Aurora)を例にとると、

resource "aws_rds_cluster" "main_db" {
  cluster_identifier              = "main_db-${terraform.workspace}"
  engine                          = "aurora-postgresql"
  engine_version                  = var.main_db_engine_version
  db_subnet_group_name            = aws_db_subnet_group.main.name
  vpc_security_group_ids          = [aws_security_group.main_db.id]
  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.main_db.name
}

resource "aws_rds_cluster_instance" "main_db" {
  for_each           = var.main_db_instances
  identifier         = "${each.key}-${each.value.az}"
  cluster_identifier = aws_rds_cluster.main_db.id
  instance_class     = each.value.instance_class
  engine             = "aurora-postgresql"
  availability_zone  = each.value.az
  promotion_tier     = each.value.promotion_tier
}

このようにworkspaceでクラスタ名を分けつつfor_eachを使ってクラスタインスタンスをそれぞれ設定できるようにします。 workspace(環境)によって分けるtfvarsは以下のようにします。

# staging
main_db_engine_version = "13.12"

main_db_instances = {
  "user01" = {
    az = "ap-northeast-1c"
    instance_class = "db.t4g.medium"
    promotion_tier  = 0
  }
}
# production
main_db_engine_version = "13.12"

main_db_instances = {
  "user01" = {
    az = "ap-northeast-1a"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "user02" = {
    az = "ap-northeast-1c"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "user03" = {
    az = "ap-northeast-1d"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "internal01" = {
    az = "ap-northeast-1a"
    instance_class = "db.r6g.large"
    promotion_tier  = 100 //writerの対象にしない
  }
}

こうするとstagingはインスタンス1台だけ、productionは4台作って1台はフェイルオーバ対象から除外する… といったことが柔軟にできますね!1台だけインスタンスクラスを変える、といったこともできます。

4. 作成後にterraform importよりは構築時からTerraformで作成する

めんどくさくてWebコンソールから作成してからコードにしがちですが、構築時からTerraform使ったほうがよいと考えてます。Terraform ImportだとImport漏れが起こりがちで、コードから同じ環境がきちんと再現できるかが微妙に怪しいからです。せっかくコード化するならちゃんと運用時に使えるコードにしたいものです。

こんなところでしょうか?ほかにも無意識にやってることはありそうだけれども思いつく限りでこのあたりは気を付けて使ってます。TerraformのコツはやりたいことをTerraformに落とし込むよりTerraformコードにしやすい構成にすることだと思っております……ではでは