Skip to content

perf: cache Terraform provider plugins in terraform-check - #93

Merged
bryce-lynn-nttd merged 1 commit into
mainfrom
feat/terraform-provider-plugin-cache
Aug 3, 2026
Merged

perf: cache Terraform provider plugins in terraform-check#93
bryce-lynn-nttd merged 1 commit into
mainfrom
feat/terraform-provider-plugin-cache

Conversation

@bryce-lynn-nttd

Copy link
Copy Markdown
Contributor

Summary

Adds a shared Terraform provider plugin cache to both jobs in reusable-terraform-check.yml that run terraform init (lint and tests).

Why

tfmodule/init in the module Makefile initializes the root module and every example directory separately. Without a shared plugin cache each of those directories downloads the same providers from scratch, so a module with a root plus two examples pulls azurerm or aws three times per run — repeated across every PR in ~90 skeleton-managed repos.

Setting TF_PLUGIN_CACHE_DIR collapses that to one download per run, and caching the directory across runs removes it almost entirely.

Notes

  • The directory is created explicitly, because Terraform silently ignores the plugin cache when the directory does not already exist — without the mkdir this change would be a no-op that looks correct.
  • TF_PLUGIN_CACHE_DIR is exported via $GITHUB_ENV rather than a job-level env: so the path can be derived from $HOME instead of hardcoding /home/runner.
  • Pinned to the same actions/cache commit already used by this workflow for the asdf tool cache, so there is no new action version to review.
  • Cache key is the hash of **/versions.tf with a restore-keys prefix fallback, so a provider constraint change gets a fresh entry while still warm-starting from the previous one.

Independent value

This stands on its own as a speedup for what CI already does. It also happens to be the prerequisite that makes an upcoming required_version floor check cheap — that check inits a second time with an older Terraform, and provider binaries are independent of Terraform core version, so it reuses these same cached providers rather than re-downloading. See launch-terraform-skeleton#38.

Generated with Cursor Agent (Opus 5)

Made with Cursor

The Makefile inits the root module and every example directory separately, so
each one currently downloads the same providers again from scratch. Setting a
shared TF_PLUGIN_CACHE_DIR collapses that to one download per run, and caching
the directory across runs removes it almost entirely.

Applied to both jobs that run terraform init (lint and tests). The directory is
created explicitly because Terraform silently ignores the plugin cache when the
directory does not already exist.

Co-authored-by: Cursor <cursoragent@cursor.com>
@bryce-lynn-nttd

Copy link
Copy Markdown
Contributor Author

Verified end to end

Nothing in this repo invokes reusable-terraform-check.yml, so this PR's own CI never executes the workflow it changes. I exercised it against a real module repo by temporarily pointing tf-azurerm-module_primitive-eventhub's check workflow at this branch (throwaway draft PR, since closed and branch deleted).

Results from that run:

Check Outcome
Check Terraform workflow loads and runs pass (no startup_failure)
Configure Terraform plugin cache directory step pass
Cache Terraform provider plugins step pass
Post-run cache save pass — Cache saved with key: Linux-tf-plugins-…
make lint pass

The cache is genuinely populated rather than saving an empty directory — the resulting entry is 46 MiB:

Linux-tf-plugins-0c46654…  46 MiB

And within the run, later inits stopped re-downloading:

- Using previously-installed hashicorp/azurerm v3.117.1
- Using previously-installed hashicorp/random v3.9.0
- Using previously-installed hashicorp/azurerm v3.117.1

Timing corroborates it: the first azurerm install took ~10s, the second ~1.7s. That run started with a cold cache (Cache not found for input keys), so the 46 MiB save is the benefit that accrues to subsequent runs.

One note for reviewers reproducing locally

Local results can mislead. If you have ~/.terraformrc with plugin_cache_dir set, or a ~/.terraform.d/plugins directory (an implied local mirror), Terraform symlinks the cache entry to that mirror instead of storing real files — the cache directory then shows 0 bytes and looks inert. Neither exists on a runner, which is why real files land there in CI. I chased that twice before spotting it, and it also means the cache can contain symlinks pointing outside itself in local setups, though not on a runner.

Generated with Cursor Agent (Opus 5)

@rakesh-gorige-nttd rakesh-gorige-nttd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — approved

Verified the diff adds plugin cache setup to both jobs that run terraform init (lint + tests). Good details: explicit mkdir -p (Terraform silently no-ops without it), $GITHUB_ENV instead of hardcoded paths, same pinned actions/cache as existing asdf cache, sensible key on **/versions.tf with prefix fallback.

End-to-end validation against a real module repo is convincing — cache populated (46 MiB), subsequent inits reuse providers. Standalone perf win; also sets up skeleton #38's floor check cheaply.

LGTM.

@aarti-joshi-nttd

Copy link
Copy Markdown

Review — approved

Summary

This PR adds a shared TF_PLUGIN_CACHE_DIR plus GitHub Actions cache to the lint and tests jobs in the reusable terraform-check workflow. That addresses repeated provider downloads when tfmodule/init runs against the root module and every example directory separately — a real cost across ~90 skeleton-managed repos.

The approach is sound and well-validated. LGTM.


Good things

  1. Correct problem diagnosistfmodule/init loops over ALL_TF_MODULES and ALL_EXAMPLES, so without a shared plugin cache each directory re-downloads the same providers.

  2. Critical mkdir -p detail — Terraform silently ignores TF_PLUGIN_CACHE_DIR if the directory doesn't exist. Creating it explicitly avoids a change that looks correct but does nothing.

  3. Portable env setup — Exporting via $GITHUB_ENV with $HOME instead of hardcoding /home/runner is the right pattern for GitHub-hosted runners.

  4. Consistent with existing workflow — Reuses the same pinned actions/cache@cdf6c1… commit as the asdf tool cache; no new action version to vet.

  5. Sensible cache keyhashFiles('**/versions.tf') covers root + examples (each has its own versions.tf in skeleton repos). The restore-keys prefix gives a warm start when constraints change.

  6. Step ordering is correct — Cache is configured after checkout/asdf restore and before make configure / make lint / tests, so all inits in the job benefit.

  7. Strong E2E evidence — Validated against a real module repo (tf-azurerm-module_primitive-eventhub): 46 MiB cache saved, subsequent inits show "Using previously-installed …", ~10s → ~1.7s on repeat azurerm install.


Non-blocking suggestions

Item Notes
Duplicated block The same 22-line block appears in both lint and tests. Fine for now; a small composite action in launch-workflows could DRY it up later if this pattern spreads.
Cache key could include lock files Adding .terraform.lock.hcl to the key would invalidate on provider pin changes without touching versions.tf. Not required for correctness — Terraform downloads missing versions regardless — but would reduce stale cache entries over time.
Parallel jobs don't share within a run lint and tests run on separate runners, so provider downloads in one job don't help the other in the same workflow run. Cross-run caching per repo still applies. Inherent to the current job layout, not a regression.
actions/cache vs restore/save split asdf uses cache/restore + conditional cache/save; plugin cache uses the combined action. Both are valid; the combined form fits here since you always want to persist updated plugins at job end.

Blocking issues

None.

@bryce-lynn-nttd
bryce-lynn-nttd merged commit c68b7aa into main Aug 3, 2026
5 checks passed
@bryce-lynn-nttd
bryce-lynn-nttd deleted the feat/terraform-provider-plugin-cache branch August 3, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants