Back to Blog
Terragrunt
CAS

Terragrunt: How to use the CAS

Yousif Akbar
Yousif
Akbar
,
Principal Software Engineer
July 6, 2026

Terragrunt 1.1 promoted the Content Addressable Store (CAS) to general availability, and with it shipped a new attribute: update_source_with_cas. What does this mean for you?

The TL;DR: you can now author components in your infrastructure catalog with plain relative paths, and Terragrunt will rewrite them into content-addressed cas:: references when it generates your stacks.

If you maintain a catalog of infrastructure patterns (i.e. modules, units, and stacks) that live repositories consume, this attribute removes a whole category of version plumbing from your configuration. If you use Gruntwork Pipelines, it also makes your pipelines way faster. We'll get to why soon.

The problem: versioning a layered catalog

A common (and recommended) way to organize Terragrunt at scale is to split your infrastructure into two kinds of repositories:

  • One or more catalog repositories that hold your reusable components: OpenTofu/Terraform modules, the Terragrunt units that wrap them, and the stacks that compose those units into deployable patterns.
  • Live repositories then reference the catalog(s) from terragrunt.stack.hcl and terragrunt.hcl files, where you run terragrunt commands to manage your live infrastructure.

Components in the catalog can be self-referential. If you want to define modules, units and stacks in the same catalog repository, a stack in the catalog points at units in the catalog, and those units point at modules in the catalog. The problem with this is that the terragrunt.stack.hcl file you author in the catalog gets generated into the live repository, so every source in it has to resolve in both places. A relative path like ../..//units/vpc is valid inside the catalog but meaningless once the stack has been generated somewhere else.

That constraint left catalog authors with two options, neither great:

  1. Pin every nested source to a remote URL with an explicit ?ref=. Now a single version bump means editing every layer of the catalog.
  2. Plumb a version down through values, so one version set at the top determines the ref used by every stack, unit, and module below it.

The second option became the standard workaround, and it looks something like this:

# live-repo/dev/terragrunt.stack.hcl
locals {
  version = "v1.2.3"
}

stack "network" {
  source = "github.com/acme/my-catalog//stacks/network?ref=${local.version}"
  path   = "network"

  values = {
    version = local.version
  }
}

# my-catalog/stacks/network/terragrunt.stack.hcl
unit "vpc" {
  source = "github.com/acme/my-catalog//units/vpc?ref=${values.version}"
  path   = "vpc"

  values = {
    version = values.version
  }
}

It works. One version defined at the top-level stack controls versions for the whole tree. But the interpolation is easy to get subtly wrong, and every component in the catalog now has to know about a version value whose only job is to reconstruct information Terragrunt already had when it fetched the catalog in the first place.

There's a hidden cost too. Each of those values blocks materializes as a generated terragrunt.values.hcl file on disk, and every one of those files changes when the version changes. Hold that thought; it will come up again in the Pipelines section.

What update_source_with_cas does

The CAS stores everything Terragrunt fetches, addressed by a hash of its content. Once your catalog has been fetched, there's a tree in the CAS that corresponds exactly to the catalog at the ref you asked for.

update_source_with_cas takes advantage of that. Setting it on a block opts that block into a rewrite that happens after generation: the source attribute is replaced with a cas:: reference pointing at the content-addressed tree in your local CAS.

The attribute can go in any and all of three places:

  • unit blocks in terragrunt.stack.hcl
  • stack blocks in terragrunt.stack.hcl
  • terraform blocks in terragrunt.hcl

Relative paths that are only valid inside the catalog become references that are valid anywhere, built from context Terragrunt captured when it fetched the catalog repository. A //subdir suffix on the source is preserved on the rewritten reference, so a source that selects a subdirectory keeps resolving to that same subdirectory inside the content-addressed tree.

Authoring a catalog with update_source_with_cas

Let's build the same network stack with the new attribute. The catalog has three layers:

# my-catalog/units/vpc/terragrunt.hcl
terraform {
  source = "../..//modules/vpc"

  update_source_with_cas = true
}

# my-catalog/stacks/network/terragrunt.stack.hcl
unit "vpc" {
  source = "../..//units/vpc"
  path   = "vpc"

  update_source_with_cas = true
}

The module at my-catalog/modules/vpc/ is plain OpenTofu/Terraform. Note what's absent: there's no values block and no ?ref= interpolation. Each layer references its neighbor with an ordinary relative path.

The live repository references the catalog stack with a single version pin:

# live-repo/dev/terragrunt.stack.hcl
stack "network" {
  source = "github.com/acme/my-catalog//stacks/network?ref=v1.2.3"
  path   = "network"
}

Run terragrunt stack generate in live-repo/dev and the following happens, in order:

  1. Terragrunt fetches my-catalog at v1.2.3 once, into the CAS.
  2. It generates the network stack and sees update_source_with_cas on the vpc unit inside it, so it follows the relative path within the fetched catalog and materializes that unit's tree into the CAS as well. The unit's terraform block carries the same attribute, so the module source gets the same treatment.
  3. Each generated layer is written out with its source rewritten to a cas:: reference.

The generated stack file in the live repository ends up looking like this:

# live-repo/dev/network/terragrunt.stack.hcl (generated)
unit "vpc" {
  source = "cas::sha1:f39ea0ebf891c9954c89d07b73b487ff938ef08b//units/vpc"
  path   = "vpc"

  update_source_with_cas = true
}

And the generated unit:

# live-repo/dev/network/.terragrunt-stack/vpc/terragrunt.hcl (generated)
terraform {
  source = "cas::sha1:f39ea0ebf891c9954c89d07b73b487ff938ef08b//modules/vpc"

  update_source_with_cas = true
}

The cas:: prefix is a forced-getter protocol that tells Terragrunt to resolve the source from your local CAS. These generated files are self-contained: run terragrunt plan against that unit later and it fetches straight from the CAS, with no network requests and no dependency on the surrounding repository layout.

The end result is the property the values pattern was buying without the plumbing. One ?ref= at the top of the live repository pins the entire tree, and bumping the catalog version is a one-line change.

Why this matters for Pipelines users

Terragrunt Scale has first-class support for stacks.

To decide which units within a stack need a plan or an apply, it diffs the generated files on the filesystem: if a generated file changed, the unit it belongs to gets a run.

Under the values-plumbing pattern, the version reference was written into terragrunt.values.hcl files across the entire generated tree. Bump the catalog from v1.2.3 to v1.2.4 and every one of those files changes, so Pipelines sees every unit as changed and plans all of them. That happens even when the bump was only necessary to update one module and the content of everything else is identical.

With update_source_with_cas, the generated source is derived from content. A unit whose content didn't change between catalog versions hashes to the same cas:: reference, so there's no filesystem diff and Pipelines leaves it alone. Only the units whose content actually changed get planned and applied.

On a large estate, that's the difference between planning hundreds of units on every version bump and planning the handful that changed.

This Terragrunt feature was prioritized directly due to customer feedback in Pipelines. Pipelines users told us the version plumbing was both an ergonomics headache and a source of wasted work in Pipelines, and that feedback is what pushed this work up the Terragrunt roadmap.

Things to know before adopting

  • You'll need to be using Terragrunt 1.1 or later to use this functionality, where the CAS is on by default. On 1.0.x the cas experiment has to be enabled explicitly as an opt-in experiment.
  • The update_source_with_cas attribute defaults to false, and catalog authors opt in per block. Consumers of a catalog don't set anything; the rewrite follows the attributes the catalog author wrote.
  • The CAS hardlinks fetched content into .terragrunt-cache as read-only files to protect the shared store. If your workflow edits fetched sources in place, set mutable = true on the terraform block and Terragrunt will copy files from the CAS instead.
  • It pairs well with stack dependencies (autoinclude blocks), which also graduated in 1.1. The Terragrunt 1.1 release post covers both.

Wrapping up

The new workflow allows for convenient relative paths in the catalog, one version pin in the live repository for your catalog, and content-addressed generation so that you can regenerate your stacks without issue. The CAS integration docs cover the full details.

Try it on one stack in your catalog, then check how many units actually get planned on your next version bump.

Features like the CAS and update_source_with_cas come out of the work we do supporting large Terragrunt estates. If that's the problem you're solving, check out Terragrunt Scale.

There's even a Free Tier where you can try it out today.