terraform get -update vs terraform init -upgrade

devops

terraform get -update vs terraform init -upgrade

last updated 2026-10-10Daniel Corneschi8 min read

Overview

Both commands refresh module sources, but they operate at different scopes and serve different purposes. Confusing them — or using one when you need the other — leads to stale modules, unexpected provider changes, or wasted time re-downloading things you didn’t intend to touch.

Quick Comparison

Aspectterraform get -updateterraform init -upgrade
ScopeModules onlyModules + Providers + Backend
Updates providersNoYes
Updates modulesYesYes
Updates .terraform.lock.hclNoYes (provider entries)
Initializes backendNoYes
Works with a broken or misconfigured backendYesNo (fails while initializing the backend)
Typical use caseRefresh module source codeFull dependency upgrade

terraform get

terraform get downloads and updates module source code into .terraform/modules/. That’s all it does — nothing else.

# Download modules (only if not already cached)
terraform get

# Force re-download even if already cached
terraform get -update

What it touches

.terraform/
├── modules/           ← terraform get operates HERE
│   ├── modules.json   ← manifest mapping module calls to local paths
│   └── vpc/           ← downloaded module source
└── providers/         ← NOT touched by terraform get

When -update matters

Without -update, terraform get only downloads modules that are missing from the cache. If the module source already exists in .terraform/modules/, it’s skipped — even if a newer version is available.

With -update, Terraform re-fetches all modules regardless of what’s cached:

  • Registry modules: resolves the latest version matching the constraint
  • Git modules: re-clones at the specified ref
  • HTTP/S3 archives: re-downloads the archive
  • Local modules: no effect (always read from disk)

What it does NOT do

  • Does not touch providers
  • Does not update .terraform.lock.hcl
  • Does not initialize or reconfigure the backend
  • Does not validate provider requirements

terraform init -upgrade

terraform init -upgrade is a superset that covers everything terraform get -update does plus providers and backend initialization.

terraform init -upgrade

What it touches

.terraform/
├── modules/           ← re-fetched (same as get -update)
│   ├── modules.json
│   └── vpc/
├── providers/         ← re-resolved and re-downloaded
│   └── registry.terraform.io/
│       └── hashicorp/aws/5.50.0/linux_amd64/
├── terraform.tfstate  ← backend cache (only with a backend block)
└── environment        ← selected workspace (only after selecting a non-default one)

.terraform.lock.hcl    ← UPDATED with new provider versions/hashes

The full sequence

  1. Initialize/verify backend configuration
  2. Re-resolve provider versions within declared constraints
  3. Download new provider binaries
  4. Update .terraform.lock.hcl with new versions and hashes
  5. Re-fetch all modules (same behavior as get -update)

When to Use Which

Use terraform get -update when:

  • You updated a module’s source code (pushed a new commit to a git ref you’re tracking)
  • You want to refresh modules without risking provider version changes
  • You’re iterating on a module locally referenced via git and want the latest
  • Your backend is broken/misconfigured and you just need fresh modules to work on
  • You want a fast operation that doesn’t re-resolve providers (faster in large projects)

Use terraform init -upgrade when:

  • You want to upgrade both providers and modules
  • You changed provider version constraints and need to re-resolve
  • You need a full re-initialization (backend + providers + modules)
  • You’re doing a scheduled dependency update
  • A new provider version has a fix you need

Use plain terraform init when:

  • You cloned the repo and need to set up the working directory
  • You want to install exactly what’s locked (deterministic)
  • Normal day-to-day workflow before plan/apply

Behavior with Different Module Sources

Module Sourceget -updateinit -upgrade
Registry (terraform-aws-modules/vpc/aws)Re-resolves version constraintSame
Git with tag (?ref=v2.1.0)Re-clones at same tagSame
Git with branch (?ref=main)Re-clones latest commit on branchSame
Git without refRe-clones default branch HEADSame
Local path (./modules/foo)No effect (always fresh)No effect
HTTP archiveRe-downloadsSame

For module handling, the two commands are identical. The difference is everything else init -upgrade does on top.

Provider Handling: The Key Difference

This is where the two commands diverge completely.

terraform get -update

terraform get -update
# Providers: untouched
# .terraform.lock.hcl: untouched
# Backend: untouched
# Modules: re-fetched

Your provider versions stay exactly where they were. The lock file doesn’t change. If you had aws = 5.40.0 locked, you still have aws = 5.40.0 after running this.

terraform init -upgrade

terraform init -upgrade
# Providers: re-resolved (may change versions!)
# .terraform.lock.hcl: updated
# Backend: re-initialized
# Modules: re-fetched

If your constraint says ~> 5.40 and 5.50.0 was released since your last lock, you’ll now have 5.50.0. The lock file is rewritten.

Performance Differences

In large projects with many providers the distinction matters: the AWS provider alone is about 170 MB to download and over 600 MB unpacked (5.100.0 on darwin_arm64: 664 MB).

CommandTime (rough estimate, depends on network)NetworkDisk writes
terraform get -update5–30 secondsModule sources only.terraform/modules/
terraform init -upgrade30–120+ secondsModules + provider binaries.terraform/modules/ + .terraform/providers/ + lock file
terraform init (no upgrade)5–15 secondsOnly missing itemsMinimal

If you only need fresh modules, get -update avoids downloading hundreds of megabytes of provider binaries.

Common Scenarios

Scenario 1: You pushed a fix to a shared module

Your config references a git module:

module "networking" {
  source = "git::https://github.com/org/terraform-networking.git?ref=main"
}

You pushed a fix to main. You want to pick it up:

# Fast, targeted — only refreshes modules
terraform get -update

# Then plan/apply as normal
terraform plan

No need for init -upgrade here — providers haven’t changed.

Scenario 2: New provider version fixes a bug

The AWS provider 5.50.0 fixed a bug you’re hitting. Your constraint is ~> 5.40:

# You need init -upgrade because you want a new provider version
terraform init -upgrade

# Verify the version changed
grep "aws" .terraform.lock.hcl

# Plan to ensure nothing unexpected
terraform plan

# Commit the lock file
git add .terraform.lock.hcl && git commit -m "chore: upgrade AWS provider"

terraform get -update would not help here — it doesn’t touch providers.

Scenario 3: Both module and provider need updating

You bumped a module version constraint AND a provider constraint:

# init -upgrade handles both in one command
terraform init -upgrade

There’s no advantage to running get -update separately in this case.

Scenario 4: CI pipeline — module changed but providers must stay pinned

# Refresh modules without risking provider drift
terraform get -update

# Then normal init (installs locked providers, doesn't re-resolve)
terraform init

# Plan and apply
terraform plan -out=tfplan
terraform apply tfplan

This pattern ensures modules are fresh while providers remain at their locked versions. It only adds something when .terraform/ is cached between runs: on a fresh checkout, plain terraform init already downloads the newest module versions your constraints allow.

Edge Cases and Gotchas

1. terraform get doesn’t validate provider requirements

If a new module version requires a provider you haven’t declared (or a newer version than you have locked), terraform get -update won’t warn you. You’ll only discover the problem at plan time:

Error: Inconsistent dependency lock file

The following dependency selections recorded in the lock file are
inconsistent with the current configuration:
  - provider registry.terraform.io/hashicorp/random: required by this configuration but no version is selected

To make the initial dependency selections that will initialize the dependency
lock file, run:
  terraform init

terraform init -upgrade catches this because it re-resolves the full dependency tree.

2. terraform get after init -upgrade is redundant

Since init -upgrade already re-fetches modules, running get -update afterward does nothing useful.

3. terraform get works without a configured backend

This is actually useful when you’re working on module development and don’t have (or need) a backend:

# Backend is misconfigured or you're in a module-only workspace
terraform get -update  # Works fine
terraform init         # Would fail due to backend issues

4. Neither command updates local modules

Local path modules (source = "./modules/foo") are always read directly from disk. Neither get -update nor init -upgrade copies or caches them — they’re used in place.

5. Module versions aren’t in the lock file

Even after init -upgrade, module versions are not recorded in .terraform.lock.hcl. Only providers are locked. This means:

  • terraform get -update and terraform init -upgrade can both silently pick up new module versions
  • For deterministic module versions, use exact version constraints or git refs with specific tags/commits

Decision Flowchart

Need to refresh dependencies?
│
├── Only modules changed?
│   ├── Yes → terraform get -update
│   └── No  ↓
│
├── Providers need updating?
│   ├── Yes → terraform init -upgrade
│   └── No  ↓
│
├── Backend config changed?
│   ├── Yes → terraform init -reconfigure (or -migrate-state)
│   └── No  ↓
│
└── Just need to install what's locked?
    └── Yes → terraform init

Key Takeaways

  • terraform get -update = refresh module source code only (fast, safe, no provider/lock changes)
  • terraform init -upgrade = refresh everything — modules, providers, lock file, backend (slower, changes versions)
  • Use get -update when you only care about modules and want to leave providers untouched
  • Use init -upgrade when you want to pull newer provider versions within your constraints
  • init -upgrade is a superset of get -update — it does everything get -update does plus more
  • Neither command locks module versions — only providers are locked in .terraform.lock.hcl
  • In CI, prefer get -update + init over init -upgrade to keep provider versions deterministic