← All articles

SSM Parameter Store vs Terraform state for shared values

Shared infrastructure configuration contract

A common platform problem starts with values that look too small to deserve architecture: the access gateway hostname, the name of a shared monitoring proxy, the technical Kubernetes cluster identifier, the account-wide tags every workload should carry, or a company-level constant that appears in many repositories. Everyone hardcodes the same string because it is "stable". Then one day it changes and the team has to find it in Terraform, Ansible, Helm values, shell scripts, CI jobs, ArgoCD plugins, and old migration code.

The design question is simple: where should those shared values live? The two candidates that come up most often are Terraform state and AWS Systems Manager Parameter Store. Terraform state is tempting because Terraform already knows many of the values. Parameter Store is tempting because every tool can read it through AWS APIs.

After looking at the tradeoffs, the conclusion is this: SSM Parameter Store is the better default for shared infrastructure values consumed by many tools. Terraform state should remain Terraform's private implementation record, not the company-wide configuration API.

Start with the access boundary

The strongest argument against using Terraform state as the general source of truth is not convenience. It is access control.

Terraform state often contains more than the value a consumer needs. It can contain generated passwords, provider-returned attributes, resource identifiers, rendered user data, connection strings, and other operational details. Marking an output as sensitive improves CLI display behavior, but it does not turn the state snapshot into a least-privilege data API.

HashiCorp's own documentation for terraform_remote_state warns about this shape: the data source exposes only root outputs to Terraform, but the consumer still needs access to the entire state snapshot. That is acceptable when one trusted Terraform layer reads another trusted Terraform layer. It is much harder to defend when an Ansible playbook only needs a list of tags, or a CI job only needs a hostname.

Parameter Store lets us publish only the values intended for sharing and grant access by path. A deployment role can read /company/shared/access/gateway/hostname without receiving the VPC module's full state. A monitoring role can read /company/prod/monitoring/proxy/name without seeing database passwords. That separation is the point.

State is not a contract

Terraform state is a ledger for Terraform. It records how Terraform maps configuration to remote objects and what it last observed. Treating that ledger as a general integration contract creates coupling in the wrong direction.

Once other tools read state, Terraform module structure becomes an API surface. Renaming a module, splitting a stack, moving a resource, or changing an output shape becomes a breaking change for consumers that should not care how the infrastructure code is organized. The team starts bending repository layout around readers of state instead of around ownership and deployment boundaries.

Parameter Store reverses that. Terraform can still create the resource, but it publishes a deliberate value under a deliberate name:

resource "aws_ssm_parameter" "access_gateway_hostname" {
  name  = "/company/shared/access/gateway/hostname"
  type  = "String"
  value = "access.example.com"
}

Consumers depend on the parameter path, not on Terraform's internal module graph. If the hostname later comes from Route 53, another Terraform workspace, a migration script, or a manual third-party onboarding process, the contract can stay the same.

Consumer diversity matters

Terraform can read Terraform state easily. Other tools can technically read it too because most backends store a JSON snapshot somewhere, but "technically possible" is not the same as a clean operational interface.

In a real platform, consumers are mixed:

  • Terraform modules need identifiers created by platform layers.
  • Ansible needs hostnames, tags, AMI ids, or environment metadata.
  • Shell scripts need a small value during a deployment or migration.
  • CI pipelines need shared endpoints and account conventions.
  • GitOps tooling may need to resolve placeholders before applying manifests.

For those consumers, Parameter Store is boring in the right way. The AWS CLI can read it. SDKs can read it. Terraform has a data source for it. IAM can restrict it. KMS can protect SecureString values. Parameter names can be hierarchical and discoverable. That makes it a better shared substrate than a Terraform backend object.

Do not put everything there

This rule needs a clear boundary. Parameter Store should not become a global junk drawer for every Terraform output.

Publish values when they meet at least one of these criteria:

  • The value is consumed by more than one repository, tool, or runtime boundary.
  • The consumer should not have access to the producing Terraform state.
  • The value is part of a stable platform contract, such as a shared endpoint, account convention, cluster name, network id, tag set, or integration identifier.
  • The value may need to change without humans editing many codebases.

Keep values out of Parameter Store when they are purely internal to one Terraform stack, easy to discover through a native cloud data source, or so high-volume that every application request would call SSM at runtime. Parameter Store is a configuration contract and deployment-time lookup mechanism, not a hot-path service discovery database.

Naming conventions are not optional

Automation does not remove naming conventions. It makes them more important.

A weak convention gives you a new form of hardcoding: every script now hardcodes a different parameter path. A strong convention makes the store usable by humans and automation:

/{company}/{scope}/{domain}/{name}
/{company}/{environment}/{domain}/{name}
/{company}/{project}/{environment}/{domain}/{name}

Examples:

/company/shared/access/gateway/hostname
/company/prod/platform/kubernetes/tech-cluster-name
/company/prod/monitoring/proxy/name
/company/prod/governance/default-tags

The exact path scheme matters less than consistency, ownership, and review. Each parameter should have an owner, type, expected consumers, and change process. If legacy naming is scattered across repos and environments, the answer is not to avoid naming. The answer is to introduce aliases and migration paths so new automation has a stable target while old consumers are cleaned up.

How changes should flow

The dangerous version of this idea is "let humans edit parameters in the console". That replaces hardcoded values with invisible mutable state.

The better version is GitOps-shaped:

  • Terraform, or another controlled pipeline, owns the parameter definitions.
  • Parameters are changed through pull requests unless there is an emergency path.
  • Consumers read parameters during plan, deployment, rendering, or startup.
  • Changing a shared value triggers the affected pipelines, or at least makes the required reruns explicit.
  • IAM scopes readers by path and separates readers from writers.

This gives the team the useful part of shared configuration: humans stop copying values into many codebases, and pipelines become responsible for applying the new contract. It also preserves the discipline Terraform users expect: review, history, reproducibility, and clear ownership.

Where Terraform state still fits

There is still a place for remote state. If two Terraform stacks are owned by the same platform team, share the same trust boundary, and one stack needs a structured output from another, terraform_remote_state can be pragmatic. It is especially useful during bootstrapping or inside a tightly controlled infrastructure layer.

But that should be a local decision inside Terraform, not the default integration mechanism for the whole engineering organization. The moment a non-Terraform tool needs the value, or the consumer should not receive full state access, publish an explicit parameter instead.

The practical rule

Use this rule of thumb:

If Terraform is the only consumer and the trust boundary is the same, remote state can be acceptable. If many tools consume the value, or least privilege matters, publish it to Parameter Store.

SSM is an extra dependency, but it is the right kind of dependency: a small, permissioned, tool-neutral contract. Terraform state is simpler only until the first consumer needs one harmless string but receives access to the entire infrastructure snapshot.

The goal is not to move hardcoding from code to SSM. The goal is to define a stable platform API for shared values, keep Terraform state private, and let automation read the same source without teaching every tool how the Terraform repository happens to be shaped today.