Terraform のいろいろ

Terraform の雑多なメモ。

有名どころのベストプラクティス

常にこの通りにしているわけではないですけど、参考までに。

モジュールを使う

かつて(v0.11 ごろ?)、モジュールは次のようにシンボリックリンクが作成される前提になっていたことがあって、

$ terraform version | head -1
Terraform v0.11.15

$ ll .terraform/modules/
total 12
drwxr-xr-x 2 user user 4096 2023-08-12 03:10 ./
drwxr-xr-x 3 user user 4096 2023-08-12 03:09 ../
lrwxrwxrwx 1 user user   48 2023-08-12 03:10 8175c15861ae98d01b480045c6f45ff8 -> /path/to/terraform/modules/sub/
-rw-r--r-- 1 user user  273 2023-08-12 03:10 modules.json

当時、以下のように Windows の共有フォルダを cifs でマウントした Linux サーバ上で作業していた身としては非常に相性が悪いものでした。cifs 上ではシンボリックリンクが作成できないためです。いちおう、次のような方法で cifs でマウントした中の一部のディレクトリに Linux 上の別ディレクトリをマウントさせることは出来ますが・・何らかの事情でこの方法はあまり使っていませんでした(理由は忘れた)。

ので、基本的には terraform でモジュールは使わずにフラットに 1 ディレクトリに詰め込んでいました。

v0.12 からはこれが改善されていてシンボリックリンクは使われなくなったようです(今は WSL2 の ext4 上で作業しているので cifs の制限はもう関係ないですが)。

$ terraform version | head -1
Terraform v0.12.31

$ ll .terraform/modules/
total 12
drwxr-xr-x 2 ngyuki ngyuki 4096 2023-08-12 03:15 ./
drwxr-xr-x 3 ngyuki ngyuki 4096 2023-08-12 03:15 ../
-rw-r--r-- 1 ngyuki ngyuki  105 2023-08-12 03:15 modules.json

ので、今は適宜モジュールに分割しています。記述量は増えますが同じようなリソースを複数作るときに DRY にできますし、リソース識別子も名前の衝突をあまり気にせずに「とりあえず main でいいか」と思えます。

ちなみに terraform のオフィシャルの aws 関係のモジュールだと this が良く使われているようです。

terraform-aws-modules/terraform-aws-vpc/main.tf

これをまねて一時的に this を使ってたこともあるのですが、今はやっぱり main で良いかなと思っています。

terraform workspace は使わずディレクトリで分ける

Terraform で本番やステージングなどの変数定義

基本的に「案.5 Workspaces を使わずにディレクトリで分ける」一択で良いと思います。↑にも記載の通り workspace は次の2点が微妙だと思ってます。

  • workspace の切り替えに terraform workspace select のひと手間が必要
  • 環境によってあったりなかったりするリソースで count が必要で見通しが悪い

モジュールを使わずに 1 ディレクトリに詰め込んでいたときは環境をディレクトリで分けてしまうと tf ファイルの重複が酷いことになってしまいますが、モジュールを使うならモジュールで共通化できるので重複は最小限にできます。

環境ごとに重複するファイル

workspace は使わずにディレクトリで tfstate を分ける場合、どうしてもルートモジュールに必要な terraform や provider や module などのディレクティブは重複します。次のようなツールを使うという手もありますが、既存の構成からの移行が簡単ではなさそうだったので使っていません。

ので、どうしても重複を除きたいならシンボリックリンクで次のようにしています。

envs/
    common/
        main.tf
        provider.tf
        terraform.tf
    prd/
        main.tf      -> ../common/main.tf
        provider.tf  -> ../common/provider.tf
        terraform.tf -> ../common/terraform.tf
        backend.tf
        locals.tf
    stg/
        main.tf      -> ../common/main.tf
        provider.tf  -> ../common/provider.tf
        terraform.tf -> ../common/terraform.tf
        backend.tf
        locals.tf
    dev/
        main.tf      -> ../common/main.tf
        provider.tf  -> ../common/provider.tf
        terraform.tf -> ../common/terraform.tf
        backend.tf
        locals.tf

ただ、IaC はベタに書いても良いように思うので、今はこれぐらいならベタに書いています。

モジュールをリソースの種類(AWS のサービス)ごとに分けようとしない

これはもともとそんなことをしていたわたしが変だと思うので一般的なことではないと思いますが・・

モジュールを使わずにフラットに 1 ディレクトリに詰め込んでいた時は AWS のサービスごとに tf ファイルを設けていました(iam.tf とか)。それをそのままモジュールに移した結果、モジュールも AWS のサービスごとに分けようとしていました。ただ、それだとモジュール間の依存関係が双方向に入り乱れて非常に判りずらいです。

ので、関連性の強いリソースが1つのモジュールに収まるように意識するべきです。例えば次のようなモジュールは AWS サービスを超えてセットになるのが自然です(時と場合による)。

  • CloudFront 関係
    • Route53 ALIAS レコード(CF用)
    • ACM 証明書
    • Route53 CNAME レコード(ACM検証用)
  • ELB 関係
    • Route53 ALIAS レコード(CF用)
    • ACM 証明書
    • Route53 CNAME レコード(ACM検証用)
  • Lambda 関係
    • Lambda に設定する IAM Role
    • Lambda のトリガとなる SNS サブスクライブ

よく考えれば普通のことですね。。。

また、ある IAM Role にあるリソースへの許可のポリシーを付与したいとき、IAM Role を作成するモジュールに許可したいリソースの arn を与えるのではなく、リソースを作成するモジュールに IAM Role を与える方がスッキリします。

// IAM Role のリソース
resource "aws_iam_role" "main" {
    // ...snip...
}

output "role" {
    value = aws_iam_role.main.id
}
// SQS のリソース
variable "role" {
    // ...snip...
}

resource "aws_sqs_queue" "main" {
    // ...snip...
}

resource "aws_iam_role_policy" "main" {
    role = var.role
    // ...snip...
        "Resource": aws_sqs_queue.main.arn,
    // ...snip...
}

アプリで使用する IAM Role(EC2 インスタンスや ECS Service に付与する IAM Role)を作成するモジュールは色々な AWS リソースへのアクセスを許可するために大量の引数を持ちがちですが、この方法ならそれがありません。アクセスされる側のリソースで「アプリからアクセスされる」と宣言するニュアンスです。

一方で IAM 関係はセキュリティやら何やらでいろいろ言われることもあるので(変更時に申請が必要だったり)、一か所にまとまっていた方が便利なこともあって、これも時と場合によりそうです。

適宜 tfstate を分ける

次のようなリソースは無理やり1つの環境のディレクトリに入れようとはせずに適宜ディレクトリを分けて tfstate を分ける方が良さそうです。

複数環境で共通するリソース

ありがちなのが Route53 ゾーン。また、プロジェクトによっては prd 以外(stg と dev など)で VPC が共通ということもあると思います。そのようなリソースを代表となる1つの環境で作成し、その他の環境では data で参照する、という構成にもできると思います。

# envs/prd/main.tf
module "dns" {
    source    = "../../modules/dns"
    create    = true
    zone_name = local.zone_name
}
# envs/stg/main.tf
module "dns" {
    source    = "../../modules/dns"
    create    = false
    zone_name = local.zone_name
}
# modules/dns/main.tf
resource "aws_route53_zone" "main" {
    count = var.create ? 1 : 0
    name  = var.zone_name
}

data "aws_route53_zone" "main" {
    count = var.create ? 0 : 1
    name  = var.zone_name
}

output "zone_id" {
    value = var.create ? aws_route53_zone.main[0].id : data.aws_route53_zone.main[0].id
}

変に複雑で見通しが悪くなるので素直に別のディレクトリで tfstate を分ける方が良いです。例えば次のように envs と同じ階層に base を設けてその中に共有するリソースのためのディレクトリを設けたりしていました。

base/
    aaa/
    bbb/
envs/
    prd/
    stg/
    dev/

もしくは次のように envs/ に入れても良いかもしれません。

envs/
    base-aaa/
    base-bbb/
    {project}-prd/
    {project}-stg/
    {project}-dev/

更新頻度が異なる

機能的な改修とは無関係に頻繁に更新が必要になるようなリソースは他のリソースとは tfstate を分けておくと良いです。例えば WAF で IP 制限をしている都合でシステムの構成変更とは無関係に変更が頻繁に入ったり、CloudWatch Alarm も運用中に調整が入ることもしばしばなので、次のようにそれらを分けたりです。

envs/
    {project}-prd/
    {project}-prd-waf/
    {project}-prd-alarm/
    {project}-stg/
    {project}-stg-waf/
    {project}-stg-alarm/
    {project}-dev/
    {project}-dev-waf/
    {project}-dev-alarm/

巷ではもっと細かく、例えばストレージ系(RDS とかそういうの)とそれ以外で分けたりもあるのでしょうか。

aws_vpc_security_group_ingress_rule や aws_route を使う

aws_security_group の ingress や egress、および aws_route_table の route は、インラインでルールを記述できて便利ですが、基本的には使用せず、代わりに aws_vpc_security_group_ingress_rule や aws_route を使用します。

aws_security_group や aws_route_table のインラインでルールを記述していると、ルールの変更時に「全削除→全追加」のような plan が表示されます。

実際に呼ばれている API を CloudTrail で確認したところ plan の表示とは異なり差分の分しか変更されていないのですが、本番系の環境でこの plan は心理的な負荷が大きすぎるので、ルール単位のリソースである aws_vpc_security_group_ingress_rule や aws_route を使っておいた方が気が楽です。

moved や import は別ファイルに分ける

リファクタリングのために moved ブロックや import ブロックをつかうことがありますが、moved.tf や import.tf のように別ファイルに分けて記述しています。後で不要になってからファイルごと削除するためです。

default_tags に何か入れておく

provider の default_tags にはとりあえず何か入れておくと良いと思います。

provider "aws" {
  region              = local.region
  allowed_account_ids = local.allowed_account_ids
  default_tags {
    tags = {
      Project    = local.project
      Env        = local.env
      Repository = local.repository
      ManagedBy  = "terraform"
    }
  }
}

また、例えば検証とかでちょっと何か作るときとかも、共用の AWS アカウントならとりあえず名前でも入れておくと良いと思います。「これ消していいやつー?」を誰に聞けばいいかわかりやすいので。

provider "aws" {
  default_tags {
    tags = {
      Author = "ngyuki"
    }
  }
}

さいごに

よく見たら1年以上前に書いた後、しばらく寝かせておこうとして1年以上寝かせてた記事でした。腐ってはいないと思います。