Managing infrastructure using code is accepted best practice in the cloud. Terraform is the most popular cross cloud framework for infrastructure as code, but it can present challenges when dealing with resources that are updated by external processes. This is particularly true for AWS Systems Manager Parameters when parameters are modified by other applications or workflows. The tension between declarative state and mutable runtime values creates a recurring operational pattern where Terraform wants to enforce the last known configuration while the real world continues to change the parameter value behind it. The reference material explores how to use Terraform to create and manage SSM Parameters while allowing external updates seamlessly. This ensures your infrastructure is tracked while your Terraform state stays in sync with reality. The core mechanism demonstrated is Terraform's lifecycle meta argument and how it can be used to solve this challenge.
The problem space is not theoretical. Teams routinely need an initial value for a parameter so that dependent resources can be created, and then they need a separate process, often CI/CD, secrets rotation, or an application, to own the live value thereafter. Without a guardrail, each terraform apply will revert the value to the initial value defined in code, which defeats the purpose of external ownership. The article shows the full lifecycle from creation, through external mutation, to the drift that Terraform detects, and finally to the stabilization achieved by ignoring changes to the value attribute.
The External Update Challenge for SSM Parameters
There are times when you need to set an initial value for a SSM Parameter, but another process will maintain it. When using Terraform, each time you run terraform apply, the value will be reverted to the initial value.
One way to deal with this is to create the Parameter directly in the console or the AWS CLI. This isn't ideal as the param isn't tracked. We want to avoid this.
The impact of this reversion is immediate and disruptive. A team that updates a database password via a secrets manager or rotates an API key through a pipeline will see Terraform overwrite the change on the next plan. The real world consequence is credential churn, failed deployments, and loss of trust in IaC. The parameter becomes a source of conflict rather than a source of truth.
The reference example names a resource aws_ssm_parameter.example with name = "example" and type = "String" and value = "set by terraform". The first apply creates the parameter:
+ resource "aws_ssm_parameter" "example" {
+ arn = (known after apply)
+ data_type = (known after apply)
+ id = (known after apply)
+ insecure_value = (known after apply)
+ key_id = (known after apply)
+ name = "example"
+ tags_all = (known after apply)
+ tier = (known after apply)
+ type = "String"
+ value = (sensitive value)
+ version = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ ssm_param_value = "set by terraform"
The apply completes:
aws_ssm_parameter.example: Creating...
aws_ssm_parameter.example: Creation complete after 2s [id=example]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
ssm_param_value = "set by terraform"
The CLI confirms the state:
dave@laptop:/home/dave/terraform/examples/ssm$ aws ssm get-parameter --name example
{
"Parameter": {
"Name": "example",
"Type": "String",
"Value": "set by terraform",
"Version": 1,
"LastModifiedDate": "2024-09-17T08:03:23.914000+10:00",
"ARN": "arn:aws:ssm:us-east-1:012345678910:parameter/example",
"DataType": "text"
}
}
To simulate our value being updated by another process, we can update the value using the AWS CLI.
Once the external process changes the value, a subsequent Terraform run detects drift. The output shows:
aws_ssm_parameter.example: Modifications complete after 1s [id=example]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
Outputs:
ssm_param_value = "set by terraform"
The value has reverted to set by terraform. The real world consequence is that the external update is lost. The contextual layer connects this to shared parameters used as a contract between systems. The article notes a benefit of using a shared SSM Parameter like this is that we can reference the current value of the param. When terraform refreshes its state, it pulls the current value of aws_ssm_parameter.example.value from SSM.
A concrete example given is GitHub Actions building docker images and pushing them to ECR. Actions could update the current stable tag in SSM. Terraform could pull this value when deploying our ECS task or Lambda function.
While a aws_ssm_parameter data source could be used here, it has some downsides. Terraform fails if the data source doesn't exist. This means we can't create the ECR repo and other resources until the param exists. We've gone full circle and are back to creating the value manually, which we want to avoid.
Regular rotation of credentials and other secrets is an important security hygiene habit. Tracking and managing infrastructure as code ensures consistency in environments. Using lifecycle arguments for SSM Params allows us to do both, without conflict or surprises.
Terraform Lifecycle Meta Argument to Ignore Value Drift
Terraform's lifecycle meta argument to the rescue. When we use the ignore changes argument, Terraform will ignore changes for any of the listed properties.
Here is an example main.tf file showing the use of the lifecycle argument to ignore changes to the value property.
```
resource "awsssmparameter" "example" {
name = "example"
type = "String"
value = "set by terraform"
lifecycle {
ignore_changes = [
value,
]
}
}
```
The output is defined to show the value after each update:
output "ssm_param_value" {
# This isn't needed. I'm using it to show the value after each update.
value = nonsensitive(aws_ssm_parameter.example.value)
}
Testing:
When we run terraform apply for the first time, we create the SSM param with the initial value.
dave@laptop:/home/dave/terraform/examples/ssm$ terraform apply -auto-approve
Terraform used the selected providers to generate the following execution plan
The impact layer is that the initial creation is still tracked. Terraform records the resource in state, manages its name, type, tags, tier, and ARN. The value is seeded. After that, any external mutation is tolerated. The state refresh will still read the current value from SSM, so references to aws_ssm_parameter.example.value reflect reality even though Terraform will not try to overwrite it.
The contextual layer ties this to operational workflows. A team can now create a parameter with a placeholder, allow a secrets rotation Lambda to update it, allow a CI pipeline to write a tag, and still have Terraform manage the parameter's existence and metadata without fighting the pipeline.
CloudPosse SSM Parameter Store Module Patterns
Terraform module for providing read and write access to the AWS SSM Parameter Store.
Tip
- AWS Details on what values can be used
- AWS API for PutParameter
- Terraform awsssmparameter resource page
- Terraform awsssmparameter data page
For a complete example, see examples/complete.
This example creates a new String parameter called /cp/prod/app/database/master_password with the value of password1.
module "store_write" {
source = "cloudposse/ssm-parameter-store/aws"
# Cloud Posse recommends pinning every module to a specific version
# version = "x.x.x"
parameter_write = [
{
name = "/cp/prod/app/database/master_password"
value = "password1"
type = "String"
overwrite = "true"
description = "Production database master password"
}
]
tags = {
ManagedBy = "Terraform"
}
}
This example reads a value from the parameter store with the name /cp/prod/app/database/master_password
module "store_read" {
source = "cloudposse/ssm-parameter-store/aws"
# Cloud Posse recommends pinning every module to a specific version
# version = "x.x.x"
parameter_read = ["/cp/prod/app/database/master_password"]
}
Important
In Cloud Posse's examples, we avoid pinning modules to specific versions to prevent discrepancies between the documentation and the latest released versions. However, for your own projects, we strongly advise pinning each module to the exact version you're using. This practice ensures the stability of your infrastructure.
The impact of using a module is reduced boilerplate and consistent tagging. The read/write split allows a codebase to create parameters in one place and consume them elsewhere without coupling to the creation logic. The contextual layer connects this to enterprise governance where ManagedBy tags and overwrite flags are used to audit who owns a parameter.
Terraform AWS Modules SSM Parameter Wrapper
Terraform module which creates AWS SSM Parameters on AWS.
- One of multiple SSM Parameters can be created
- Value type guesser
- Allow SSM Parameter to ignore changes in the value
- Wrapper module which allows managing multiple resources with less code
The module supports simple string parameters.
module "string" {
source = "terraform-aws-modules/ssm-parameter/aws"
name = "my-parameter"
value = "some-value"
}
Secure strings are supported with secure_type.
module "secret" {
source = "terraform-aws-modules/ssm-parameter/aws"
name = "my-secret-token"
value = "secret123123!!!"
secure_type = true
}
List parameters use values not value.
module "list" {
source = "terraform-aws-modules/ssm-parameter/aws"
name = "my-list-parameter"
values = ["item1", "item2"] # "values" not "value"
}
Ignoring value changes is built into the module.
module "list" {
source = "terraform-aws-modules/ssm-parameter/aws"
ignore_value_changes = true
name = "my-parameter-ignore-value-changes"
value = "some-value"
}
The locals example shows a richer set of configurations.
locals {
parameters = {
########
# String
########
"string_simple" = {
value = "string_value123"
}
"string" = {
type = "String"
value = "string_value123"
tier = "Intelligent-Tiering"
allowed_pattern = "[a-z0-9_]+"
}
##############
# SecureString
##############
"secure" = {
type = "SecureString"
value = "secret123123!!!"
tier = "Advanced"
description = "My awesome password!"
}
"secure_encrypted_true" = {
secure_type = true
value = "secret123123!!!"
key_id = "c938de44-1c09-4c91-89fd-b5881f06f317"
}
############
# StringList
############
"list_as_autoguess_type" = {
values = ["item1", "item2"]
}
"list_as_jsonencoded_string" = {
type = "StringList"
value =
The impact layer is that teams can define many parameters from a single map, reduce duplication, and explicitly choose tiers such as Intelligent-Tiering or Advanced, which affect cost and throughput. The contextual layer links allowedpattern to validation rules that prevent non-conformant values from being written by external processes, and keyid to customer managed KMS keys for secrets.
Placeholder Creation with Manual Rotation Workflow
A common pattern is to create the parameter with a placeholder value in Terraform and then update the actual value manually or through a separate secrets management workflow.
```
Create with placeholder, update manually after
resource "awsssmparameter" "apikey" {
name = "/myapp/production/externalapi_key"
type = "SecureString"
value = "placeholder-update-after-creation"
description = "API key for the external payment service"
lifecycle {
ignore_changes = [value] # Don't overwrite manual updates
}
}
```
Using a Custom KMS Key
By default, SecureString uses the AWS managed aws/ssm key
The impact is that the parameter exists in Terraform state from day one, so dependent resources can reference it safely. The placeholder satisfies the initial creation requirement without leaking real secrets into version control. The lifecycle ignore_changes prevents Terraform from reverting the real secret once an external rotation process updates it.
The contextual layer connects this to compliance requirements where secrets must never be stored in Terraform configuration. The placeholder pattern plus ignore_changes satisfies both IaC tracking and secret hygiene.
Security Hygiene and Shared Parameter Consumption
The reference material repeatedly emphasizes that regular rotation of credentials and other secrets is an important security hygiene habit. Tracking and managing infrastructure as code ensures consistency in environments. Using lifecycle arguments for SSM Params allows us to do both, without conflict or surprises.
When terraform refreshes its state, it pulls the current value of aws_ssm_parameter.example.value from SSM. This means Terraform can read the latest rotated credential for use in tasks or Lambda functions without needing to re-apply to update the value.
The data source downside remains relevant. Terraform fails if the data source doesn't exist. This means we can't create the ECR repo and other resources until the param exists. The solution is to create the parameter as a managed resource with a placeholder and ignore_changes, rather than relying on a data source that must pre-exist.
The CloudPosse module provides both write and read capabilities, allowing a single module source to be reused for creation and consumption. The Terraform AWS Modules wrapper adds value type guessing and ignorevaluechanges flags, making it easier to manage lists and secure strings at scale.
The combination of lifecycle ignore_changes, placeholder creation, and module wrappers gives teams a durable pattern: Terraform owns the parameter's metadata and existence, external systems own the live value.
Conclusion
Terraform SSM Parameter management sits at the intersection of declarative infrastructure and mutable operational data. The core challenge is that Terraform naturally wants to converge reality to the configuration, which conflicts with parameters that are intentionally mutated outside of Terraform. The reference material demonstrates a clear resolution path using the lifecycle meta argument with ignore_changes = [value]. This allows an initial value to be set by Terraform, after which external updates are preserved and Terraform will still refresh its state to reflect the current value.
The practical workflow begins with creating the parameter in Terraform with a placeholder or initial value, protecting it with lifecycle ignore_changes, and then allowing CI/CD, secrets rotation, or application processes to update the value freely. Modules from CloudPosse and Terraform AWS Modules abstract this pattern further, providing write and read modules, value type guessing, tier selection, KMS key integration, and bulk parameter definitions via locals.
The operational impact is significant. Teams avoid the anti-pattern of creating parameters manually in the console, which removes tracking. They avoid drift fights where Terraform reverts external updates. They retain the ability to reference the current parameter value in Terraform for downstream resources such as ECS task definitions or Lambda environment variables, while still respecting the ownership boundary of external processes.
Security hygiene benefits from this approach because secrets can be rotated without touching Terraform code, and the SecureString type can be used with customer managed KMS keys. The default AWS managed aws/ssm key is mentioned as the baseline for SecureString, with custom keys available for enhanced control.
In the long run, the pattern scales. A single Terraform configuration can provision dozens of parameters using a map driven module, each with appropriate tier, allowed pattern, and ignore value settings. The state remains accurate, the external systems remain autonomous, and the infrastructure remains auditable and reproducible.