Template Engine Upgrade Drops the End-of-Life AWS SDK from Atmos
Most security findings are fixed with a patch or minor release of the affected dependency. Some need a major version upgrade instead: when a dependency's major release line stops being maintained, its CVEs stay open forever, and every project that pulls it in inherits them. That is where Atmos was: the template engine it embeds, Gomplate, had moved on from the release line Atmos used, and that line depended on the AWS SDK for Go v1, which has reached end of support and carries CVEs no v1 release will ever fix. Atmos never executed the affected code, but no patch or minor release could clear the findings. The CVEs forced our hand: the template engine now runs on Gomplate v5, two major versions ahead, and the end-of-life SDK is gone from the Atmos build entirely.
The Problem
Atmos uses Go templates in many places: stack manifests, vendor manifests, scaffolding,
atmos docs generate, Helm and Kubernetes component templates, and generated backend and varfile
paths. Gomplate supplies most of the template functions and all of the datasources.
Atmos releases used Gomplate v3. Gomplate v3 and v4 are no longer maintained, and both use the AWS SDK for Go v1. AWS no longer supports that SDK, so its vulnerabilities (GO-2022-0635 and GO-2022-0646) will never get a fix.
Dependency scanners and OpenSSF Scorecard report a vulnerable library even when your code never calls it. So every Atmos release showed these findings. The only supported fix was Gomplate v5. Atmos goes straight from v3 to v5, so this one upgrade brings the breaking changes of v3 → v4 → v5 together.
The Fix
Atmos now embeds Gomplate v5, which is built on the AWS SDK for Go v2. Upgrading was not optional, but breaking your stacks along with it would not have been acceptable either. Despite the sheer number of breaking changes across Gomplate v3 → v4 → v5, we went through them one by one and kept backward compatibility everywhere it made sense to: we built a compatibility test suite that runs the same templates through the previous release and this one and compares the results, and added a shim for every removed function we could restore faithfully. Most stack templates keep working unchanged, and Sprig functions are still layered on top by default. Each shim logs a warning naming the replacement the first time a stack manifest calls a removed function, so you can migrate on your own schedule. A handful of datasource behaviors changed in ways Atmos cannot paper over with a shim — what to check in your stacks below covers each one. What changed unconditionally is what ships inside the binary: the AWS SDK for Go v1 is no longer present in any form.
One HashiCorp Vault authentication library still pulled the old SDK in its latest tagged release, even though its development branch had already been ported to v2. Atmos pins that library to the ported development revision until HashiCorp tags a release. That pin is temporary. Dependabot will propose the bump automatically as soon as a tagged release exists, and nothing about how you use Vault datasources changes in the meantime.
The same upgrade moved the Go cryptography library to a release that closes two SSH-related denial-of-service advisories (GO-2026-6354 and GO-2026-6355). This is preventative hygiene, not a response to an exploited issue, but it is one more finding that will no longer appear on a scan of Atmos.
What to Check in Your Stacks
Gomplate v5 removed functions it had deprecated in v3 and changed some datasource behavior. Atmos keeps the removed names working through compatibility shims. Each shim logs a warning that names its replacement. A compatibility test suite runs the same templates through a v3 build and this release and compares the results; the sections below list the differences it found. The shims will be removed in a future major release, so replace the old names now. The templates configuration reference and the datasources reference carry the same list.
Renamed functions
Deprecated function names were removed. Replace them with their namespaced equivalents:
# Before
enabled: '{{ conv.Bool .vars.flag }}'
names: '{{ conv.Slice "a" "b" }}'
cfg: '{{ conv.Dict "k" "v" }}'
has: '{{ conv.Has .vars "key" }}'
sorted: '{{ strings.Sort .vars.list }}'
ip: '{{ net.ParseIP .vars.addr }}'
cidr: '{{ net.ParseIPPrefix .vars.cidr }}'
# After
enabled: '{{ conv.ToBool .vars.flag }}'
names: '{{ coll.Slice "a" "b" }}'
cfg: '{{ coll.Dict "k" "v" }}'
has: '{{ coll.Has .vars "key" }}'
sorted: '{{ coll.Sort .vars.list }}'
ip: '{{ net.ParseAddr .vars.addr }}'
cidr: '{{ net.ParsePrefix .vars.cidr }}'
The old net functions still return their v3 types (netaddr.IP, netaddr.IPPrefix, and
netaddr.IPRange), so method calls such as .Contains and .Prior keep working. The
replacements return net/netip types, which have different methods.
The unqualified aliases slice, contains, hasPrefix, hasSuffix, split, splitN, and
trim are no longer registered by Gomplate, but your templates keep working with no action needed:
slice,contains,hasPrefix,hasSuffix,split, andtrimresolve to Sprig, as they did before, because Atmos layered Sprig over Gomplate's aliases. If you have disabled Sprig (templates.settings.sprig.enabled), an Atmos shim restores Gomplate's versions. Forslice, that is Gomplate's list builder (slice "a" "b"), not Go's builtin.splitNhas no Sprig equivalent, so a shim always provides it.
Replace shimmed names with their strings.* or coll.* forms when you get the chance, and check
the argument order as you go. With Sprig disabled, the shims keep Gomplate v3's order, with the
input first, while the strings.* functions take the input last so they work in pipelines.
contains "alphabet" "pha" becomes strings.Contains "pha" "alphabet", and
splitN "a,b,c" "," 2 becomes strings.SplitN "," 2 "a,b,c". slice becomes coll.Slice with
the same arguments.
Conversions and regular expressions now fail loudly
Conversion functions such as conv.ToInt64, conv.ToFloat64, and conv.Atoi used to return 0
when handed something they could not convert. They now return an error, which surfaces as a
template processing failure; Atmos does not paper over this one, since silently returning 0 for
bad input is exactly the kind of behavior worth losing. The error message spells out the fix. If a
stack relied on the silent zero, check that the input is a number before converting it:
retries: '{{ $r := conv.ToString (index .vars "retries") }}{{ if regexp.Match "^-?[0-9]+$" $r }}{{ conv.ToInt64 $r }}{{ else }}0{{ end }}'
An empty or non-numeric value renders 0, and a whole number converts as before. The example reads
the key with index, which returns nothing for a missing key, so a missing retries also renders
0, even where Atmos treats missing values as errors (for example, in an import rendered with
context). Writing .vars.retries instead would fail on a missing key there before the check runs.
Similarly, the regexp.* functions return an error for an invalid pattern instead of panicking.
The failure is the same as before from your point of view, but the message now names the template
and the pattern.
Datasource subpaths are relative URLs
A second argument to datasource is resolved as a relative URL against the datasource's url.
Directory datasources must end with a trailing /, and a subpath that begins with / replaces
the base path rather than appending to it. Atmos warns when it sees a datasource used this way
without a trailing slash on its URL, but it cannot know whether you meant the old or the new
resolution, so the warning is a prompt to check the stack, not an automatic fix:
settings:
templates:
settings:
gomplate:
datasources:
configs:
url: "file:///configs/" # trailing slash marks a directory
components:
terraform:
app:
vars:
name: '{{ (datasource "configs" "app.yaml").name }}' # reads /configs/app.yaml
AWS Parameter Store values are returned directly
An aws+smp:// datasource returns the parameter's value itself rather than an object with a
.Value field. Drop the field access, and if the parameter holds a JSON document, tell the
datasource to parse it with the type query parameter. Atmos recognizes a configured aws+smp://
datasource read with .Value still attached and warns about it, without guessing at what the
parameter actually contains:
settings:
templates:
settings:
gomplate:
datasources:
database:
url: "aws+smp:///myapp/database/config?type=application/json"
components:
terraform:
app:
vars:
# Before: '{{ (datasource "database").Value.host }}'
db_host: '{{ (datasource "database").host }}'
Smaller datasource changes
Consul directory listings (a consul:// URL ending in /) return a list of key names instead of
a map. BoltDB datasources were removed upstream and are no longer available.
S3 datasources reject an object whose checksum does not match, instead of returning its content.
In env://NAME, NAME is the URL host, so older releases read no variable and returned empty
data. That form now fails. Use env:NAME or env:///NAME.
Vault KV v2 datasources detect the mount and add the /data/ API segment themselves. Remove
/data/ from the URL (.../kv/config, not .../kv/data/config). The result is the secret's data
without the data and metadata wrapper, so read fields directly.
Consul, Vault, SSM, and S3 datasources send different requests to those services. For example, Vault reads first check the mount type. The values your templates read do not change, but access policies or proxies that allow only specific requests may need an update. HTTP datasources still make one GET request per read, as before.
The datasource timeout in atmos.yaml (templates.settings.gomplate.timeout) now applies. Older
releases replaced it with the 5-second default whenever a stack manifest did not set its own
timeout, so a shorter global timeout was ignored. If a datasource takes longer than your global
timeout, raise the timeout in atmos.yaml or in that stack's settings.
New: coll.JQ and tmpl
coll.JQ runs a jq expression on a value, and the tmpl namespace (tmpl.Inline, tmpl.Exec)
renders nested templates. Neither was available in templates before this release.
New: semantic version helpers
Gomplate v5 adds a semver.* namespace for parsing and comparing versions inside templates, for
example {{ semver.CheckConstraint ">= 1.5.0" .vars.version }}. See the
Gomplate semver reference for the full list.
Get Involved
We will fix forward. If a template that worked before this release fails after it, or quietly renders a different value, and the cause is not on the list above, open an issue with the template expression and the error. Where a gap can be closed with another compatibility shim, we will close it in a patch release. The goal of this upgrade was to change what ships in the binary, not what your stacks can do.
