0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraform 1.17 の -minimal-refresh でも data source は毎回読まれる

0
Posted at

2026/11/07 時点の検証結果です。Terraform v1.17.1、hashicorp/aws v6.72.0 を使用しています。

Terraform 1.17 で使用できるようになった -minimal-refresh で速くなる話を見て、ウキウキで利用してみました。
しかし大きめの Terraform 実装で -minimal-refresh の効果を確認したところ、思ったより速くなりませんでした。

その実装では data source を多く使っていたので、手元で再現して調べてみると、-minimal-refresh を付けても data source は毎回読み込まれることが分かりました。data source が多いほど、効果が下がっていきます。
最初に試した実装で速くならなかったのも、おそらく同じ理由だと思います。

本記事では、料金のかからない AWS リソース(VPC、サブネット、Security Group)だけで、この挙動を確認した結果を整理します。

結論

  • -minimal-refresh を付けると、変更予定のない resource の refresh はされなくなる(API リクエストは 0 件)
  • ただし data source は、-minimal-refresh を付けても毎回読み込まれる。-refresh=false でも同じ
  • そのため data source が多いほど、-minimal-refresh の効果は小さくなる
  • data source を減らす(呼び出し側で 1 回だけ引く、変数で受け取る)と、-minimal-refresh がしっかり効くようになる

100 サービス分のモジュール(resource 300 個)を plan したときの結果です。
「API の時間」は、plan のうち resource の refresh と data source の読み込みにかかった時間です(計測方法は後述)。

モジュールの書き方 data source の
リクエスト数
API の時間
terraform plan
API の時間
terraform plan -minimal-refresh
モジュールの中で data source を引く 600 22.4 秒 18.4 秒
呼び出し側で 1 回だけ data source を引く 6 2.9 秒 0.6 秒
VPC の情報を変数で受け取る 0 2.9 秒 0 秒

デフォルトの並列度(-parallelism=10)での中央値です。
1 つ目はスロットリングで結果が大きくばらついたため、-parallelism=1 でも比べています。こちらは上から順に、64.3 秒 → 39.2 秒、24.1 秒 → 0.5 秒、24.1 秒 → 0 秒でした。

ドキュメントの記載

Reduces the number of remote API requests by refreshing only resource instances that Terraform plans to change.

まず refresh せずに plan して、変更予定のある resource instance だけを refresh し、もう一度 plan する、という動作のようです。

似たオプションとして、以前からある -refresh=false があります。こちらは resource の refresh をすべて省略するオプションです。
-minimal-refresh は、変更予定のある resource だけは refresh する点が違います。
どちらも、変更予定のない resource は refresh されないので、Terraform の外で変更された内容には気づけなくなります。

...resource instance への言及はあるものの、data source を更新するかどうかは書かれていません。

CHANGELOG も下記の記載のみでした。

A new -minimal-refresh planning option has been added, which will only refresh resources that have proposed changes.

一方、data source のドキュメントには、data source は plan の際に読み込まれると書かれています。

Terraform attempts to query data sources during the planning phase,

なので、data source が plan のたびに読み込まれること自体は、data source のもともとの動作です。
ただ、同じドキュメントには「refresh phase で data source を読み込む」という書き方もあり、refresh を減らす -minimal-refresh で data source の読み込みも減るのかどうかは、ドキュメントからは判断できませんでした。

本記事では、-minimal-refresh を付けても data source の読み込みが変わらないこと、そしてそれが plan の時間にどう効くかを確かめます。

検証の構成

よくある「共通の VPC は別の state で管理していて、サービスごとのモジュールの中で data source を使って VPC を引く」構成を再現します。

  • base: 共通の VPC とサブネット(別チームが管理しているイメージ)
  • target: サービスごとのモジュールを 100 回呼ぶ。計測するのはこちら

モジュールの中では Security Group を 1 つと、ルールを 2 本(別リソース)作ります。
VPC の情報は、vpc_id などを渡さなければモジュールの中で data source を使って引き、渡せば data source を使わないようにしました。

modules/service/main.tf
variable "name" {
  type = string
}

variable "vpc_name" {
  type    = string
  default = "minimal-refresh-shared"
}

# 下記を渡さない場合は data source で VPC とサブネットを引く
variable "vpc_id" {
  type    = string
  default = null
}

variable "vpc_cidr" {
  type    = string
  default = null
}

variable "subnet_ids" {
  type    = list(string)
  default = null
}

locals {
  lookup = var.vpc_id == null
}

data "aws_vpc" "shared" {
  count = local.lookup ? 1 : 0
  tags = {
    Name = var.vpc_name
  }
}

data "aws_subnets" "private" {
  count = local.lookup ? 1 : 0
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.shared[0].id]
  }
  tags = {
    Tier = "private"
  }
}

locals {
  vpc_id     = local.lookup ? data.aws_vpc.shared[0].id : var.vpc_id
  vpc_cidr   = local.lookup ? data.aws_vpc.shared[0].cidr_block : var.vpc_cidr
  subnet_ids = local.lookup ? data.aws_subnets.private[0].ids : var.subnet_ids
}

resource "aws_security_group" "this" {
  name        = "minimal-refresh-${var.name}"
  description = "minimal-refresh experiment"
  vpc_id      = local.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "https" {
  security_group_id = aws_security_group.this.id
  cidr_ipv4         = local.vpc_cidr
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
}

resource "aws_vpc_security_group_egress_rule" "all" {
  security_group_id = aws_security_group.this.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

# 実際にはここで ECS サービスなどにサブネットを渡すイメージ
output "subnet_ids" {
  value = local.subnet_ids
}

呼び出し側では、mode 変数で下記の 3 パターンを切り替えます。resource の構成はどれも同じです。

  • module: モジュールの中で data source を引く(data source は 200 件)
  • root: 呼び出し側で 1 回だけ data source を引いて、モジュールに渡す(data source は 2 件)
  • vars: base の output を変数で渡す(data source なし)
main.tf
variable "mode" {
  type    = string
  default = "module"
}

# mode = vars のときに使う値(base の output を渡す)
variable "vpc_id" {
  type    = string
  default = null
}

variable "vpc_cidr" {
  type    = string
  default = null
}

variable "subnet_ids" {
  type    = list(string)
  default = null
}

# mode = root のときは、呼び出し側で 1 回だけ引く
data "aws_vpc" "shared" {
  count = var.mode == "root" ? 1 : 0
  tags = {
    Name = "minimal-refresh-shared"
  }
}

data "aws_subnets" "private" {
  count = var.mode == "root" ? 1 : 0
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.shared[0].id]
  }
  tags = {
    Tier = "private"
  }
}

locals {
  vpc_id     = var.mode == "root" ? data.aws_vpc.shared[0].id : var.mode == "vars" ? var.vpc_id : null
  vpc_cidr   = var.mode == "root" ? data.aws_vpc.shared[0].cidr_block : var.mode == "vars" ? var.vpc_cidr : null
  subnet_ids = var.mode == "root" ? data.aws_subnets.private[0].ids : var.mode == "vars" ? var.subnet_ids : null
}

module "service" {
  source = "./modules/service"
  count  = 100

  name       = "svc-${count.index}"
  vpc_id     = local.vpc_id
  vpc_cidr   = local.vpc_cidr
  subnet_ids = local.subnet_ids
}

計測方法

TF_LOG=DEBUG のログから、AWS API のリクエスト数と時間を取り出しました。
AWS provider のログにはリクエストごとに tf_rpc=ReadResource(resource の refresh)や tf_rpc=ReadDataSource(data source の読み込み)が付いているので、どちらのリクエストか区別できます。

時間は下記の 2 つに分けています。

  • plan 全体の時間: ログの最初の行から最後の行まで
  • API の時間: resource の refresh と data source の読み込みについて、最初のリクエストから最後のレスポンスまで

plan 全体の時間には、Terraform や provider の起動などの時間も含まれます。この部分は -minimal-refresh では変わらないうえ、計測した時間帯によって数秒変わったので、-minimal-refresh の効果は API の時間で比べています。

mode=module のとき、1 回の plan で発生するリクエストの内訳は下記のとおりでした。

API リクエスト数
resource の refresh DescribeSecurityGroups 100
DescribeSecurityGroupRules 200
data source の読み込み DescribeVpcs 100
DescribeVpcAttribute 300
DescribeRouteTables 100
DescribeSubnets 100

aws_vpc の data source は、1 件で 5 リクエストも使っています。

plan はすべて No changes の状態で実行しました。
各条件 5 回ずつ、plan の前に毎回 20 秒空けて、-minimal-refresh なしとありを交互に実行し、中央値を載せています。
特に断りがなければ、並列度はデフォルト(-parallelism=10)です。

検証

下記に検証結果を整理しましたのでご確認ください:

data source を使わない場合

まずは VPC の情報を変数で渡した場合(mode=vars)です。

plan 実行パラメータ plan 全体の時間 API の時間 resource の refresh
(リクエスト数)
data source の読み込み
(リクエスト数)
なし 7.4 秒 2.9 秒 300 0
-minimal-refresh 3.2 秒 0 秒 0 0

resource の refresh のリクエストがきれいに 0 件になり、API の時間もなくなりました。
残っている 3 秒ほどは、Terraform や provider の起動などの時間です。

モジュールの中で data source を引く場合

次に、モジュールの中で data source を使って VPC を引いた場合(mode=module)です。data source は 200 件(aws_vpc と aws_subnets が 100 件ずつ)になります。

plan 実行パラメータ plan 全体の時間 API の時間 resource の refresh
(リクエスト数)
data source の読み込み
(リクエスト数)
なし 28.3 秒 22.4 秒 300 600〜621
-minimal-refresh 24.5 秒 18.4 秒 0 600〜620

resource の refresh は 0 件になっていますが、data source の読み込みが 600 リクエストそのまま残っています。
そもそも data source を使わない場合の -minimal-refresh なしの plan(7.4 秒)のほうが、こちらの -minimal-refresh ありの plan(24.5 秒)よりもずっと速いですね。

この構成は結果のばらつきも大きく、API の時間は -minimal-refresh なしで 7.0〜65.4 秒、ありで 4.2〜33.4 秒でした。
10 回中 8 回で DescribeVpcAttribute が RequestLimitExceeded になっていて、EC2 API のスロットリングに当たっています。

API の時間が 65.4 秒かかった回のログを見ると、1 つのリクエストが下記のように再試行されていました(該当する行の先頭部分だけを抜粋しています)。

2026-11-07T14:08:59.864+0900 [DEBUG] retrying request EC2/DescribeVpcAttribute, attempt 3
2026-11-07T14:09:07.497+0900 [DEBUG] retrying request EC2/DescribeVpcAttribute, attempt 4
2026-11-07T14:09:22.063+0900 [DEBUG] retrying request EC2/DescribeVpcAttribute, attempt 5
2026-11-07T14:09:51.340+0900 [DEBUG] retrying request EC2/DescribeVpcAttribute, attempt 6

再試行の間隔が約 8 秒、15 秒、29 秒と指数バックオフで伸びていき、attempt 3 から 6 までだけで 50 秒ほど plan が止まっていました。
data source が多いと、-minimal-refresh の有無よりも「スロットリングに当たるかどうか」のほうが plan の時間に効いてくるようです。

呼び出し側で 1 回だけ data source を引く場合

モジュールの中ではなく、呼び出し側で 1 回だけ data source を引いてモジュールに渡した場合(mode=root)です。data source は 2 件になります。

plan 実行パラメータ plan 全体の時間 API の時間 resource の refresh
(リクエスト数)
data source の読み込み
(リクエスト数)
なし 7.0 秒 2.9 秒 300 6
-minimal-refresh 6.8 秒 0.6 秒 0 6

data source の読み込みは 6 リクエスト(aws_vpc が 5、aws_subnets が 1)だけになり、-minimal-refresh ありの API の時間は 0.6 秒でした。変数で渡した場合とほぼ同じ効果です。スロットリングも起きていません。

-minimal-refresh ありの plan 全体の時間が 6.8 秒と、変数で渡した場合(3.2 秒)より長く見えますが、API の時間は 0.6 秒です。
ログを見ると、差のほとんどは設定の読み込みや provider の起動にかかった時間で、計測した時間帯の違いによるものでした。

並列度を 1 にして比べた場合

モジュールの中で data source を引く構成はスロットリングで結果がばらついたので、-parallelism=1 で直列に実行し、スロットリングの影響を除いて比べてみました。

モジュールの書き方 plan 実行パラメータ plan 全体の時間 API の時間 スロットリング
モジュールの中で引く -parallelism=1 68.4 秒 64.3 秒 0〜2 回
-parallelism=1 -minimal-refresh 47.8 秒 39.2 秒 0〜3 回
呼び出し側で 1 回だけ引く -parallelism=1 28.3 秒 24.1 秒 0 回
-parallelism=1 -minimal-refresh 6.6 秒 0.5 秒 0 回
変数で受け取る -parallelism=1 26.9 秒 24.1 秒 0〜1 回
-parallelism=1 -minimal-refresh 4.6 秒 0 秒 0 回

スロットリングはほぼ起きず、結果も安定しました。
data source が少ない 2 つの構成では、-minimal-refresh で API の時間がほぼなくなっています。
一方、モジュールの中で引く構成は 64.3 秒から 39.2 秒で、約 39% の短縮にとどまりました。残った 39.2 秒は、すべて data source の読み込みです。

-refresh=false の場合

では、resource の refresh をすべて省略する -refresh=false なら、data source の読み込みも省略されるのでしょうか?
モジュールの中で data source を引く構成(mode=module)で、3 回試してみました。

plan 実行パラメータ plan 全体の時間 API の時間 resource の refresh
(リクエスト数)
data source の読み込み
(リクエスト数)
-refresh=false 19.6 秒 15.8 秒 0 600〜612

-refresh=false でも data source は読み込まれました。
少なくとも今回のように plan の時点で引数の値が分かる data source は、-refresh=false や -minimal-refresh とは関係なく、plan のたびに読み込まれるようです。
(引数が apply まで決まらない data source などは、ドキュメントにあるとおり apply まで読み込みが先送りされます)

data source が多い場合はどうするか

今回の検証では、下記の 2 つで -minimal-refresh がしっかり効くようになりました。

  • 呼び出し側で 1 回だけ data source を引いて、モジュールに渡す
  • 値を変数で渡す

呼び出し側で 1 回だけ引く方法は、VPC 側が変わっても次の plan で追従できるので、まずはこちらを試すのが良さそうです。

変数で渡す方法は、data source が 0 件になる一方で、VPC 側が変わっても値は自動では変わりません。
今回の検証では base の output を手で渡しましたが、実際には terraform_remote_state や tfe_outputs で base の output を読むことになると思います。どちらも data source ですが、読むのは state なので、EC2 の API は呼びません(こちらは検証していません)。

ほかにも、下記のような方法が考えられます(こちらも検証していません)。

  • 複数件をまとめて取得できる data source を使う(例: aws_subnet をたくさん書くのではなく aws_subnets で一括取得する)
  • state を分割して、1 回の plan で読む data source を減らす

なお、aws_iam_policy_document のように API を呼ばずに手元で計算する data source もあります。data source なら何でも遅いわけではないので、API を呼ぶものから見直すと良いと思います。

まとめ

  • -minimal-refresh を付けると、変更予定のない resource の refresh はされなくなる
  • data source は -minimal-refresh でも -refresh=false でも毎回読み込まれる(data source のもともとの動作は変わらない)
  • data source が多いと plan の時間の多くが data source の読み込みになるので、-minimal-refresh の効果は小さくなる。スロットリングで時間も読めなくなる
  • data source を減らす(呼び出し側で 1 回だけ引く、変数で受け取る)と、-minimal-refresh の効果が大きく出る

-minimal-refresh を試して思ったより速くならなかったら、data source の数を数えてみると良いかもしれません。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?