Managing AWS SSM Parameters with Terraform and External Update Allowance

Terraform adoption as the most popular cross cloud framework for infrastructure as code creates a practical tension when AWS Systems Manager Parameters are maintained by processes outside of Terraform. The reference material describes a specific workflow where Terraform creates and tracks an SSM Parameter while permitting external applications or workflows to modify the parameter value without Terraform reverting the change on the next apply. The core problem is drift enforcement. When Terraform manages a parameter value, each terraform apply run compares the desired state in configuration against the actual state in AWS and will rewrite the parameter to match the configuration. This behavior guarantees consistency for resources fully owned by Terraform, but it breaks workflows where another system is the authoritative owner of the value, such as a CI/CD pipeline updating a stable Docker tag, a secrets rotation service rotating credentials, or an operations team updating an API key manually.

The solution pattern documented across the sources relies on Terraform’s lifecycle meta argument with the ignorechanges attribute. By declaring ignorechanges = [value] on an awsssmparameter resource, Terraform will still create the parameter, manage tags, type, tier, name, and other immutable or managed attributes, but it will stop tracking changes to the value field after creation. State refresh still pulls the current value from SSM, so dependent resources can reference the live value via awsssmparameter.example.value, while the parameter itself is not overwritten.

The Challenge of SSM Parameters Updated Outside Terraform

There are times when you need to set an initial value for an 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.

The real world consequence is operational surprise. A team may run terraform apply expecting idempotency and instead discover that a manually rotated secret or a GitHub Actions workflow that updates a stable tag in SSM has been overwritten. The infrastructure appears correct in code, but the runtime value is stale. This erodes trust in infrastructure as code and forces teams to choose between tracking the parameter or allowing external updates.

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.

Creating the parameter outside Terraform removes it from version control, code review, and state management. The parameter becomes invisible to Terraform plans, cannot be referenced reliably by other Terraform resources, and creates a manual toil burden. Auditing, disaster recovery, and environment promotion all suffer because the parameter’s existence is not codified.

Lifecycle Meta Argument and Ignore Changes Pattern

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,
]
}
}

output "ssmparamvalue" {
value = nonsensitive(awsssmparameter.example.value)
}
```

The impact layer is immediate. After the first apply creates the parameter with value set by terraform, a subsequent external update via AWS CLI or another automation changes the stored value. A later terraform apply reports 0 changes to the resource, because the value change is ignored, while the output ssmparamvalue reflects the externally updated value after state refresh. The infrastructure remains tracked, and Terraform state stays in sync with reality.

Testing demonstrates the flow. When we run terraform apply for the first time, we create the SSM param with the initial value.

Terraform used the selected providers to generate the following execution plan

Resource actions are indicated with the following symbols:

```
+ create
Terraform will perform the following actions:

awsssmparameter.example will be created

  • resource "awsssmparameter" "example" {
    • arn = (known after apply)
    • datatype = (known after apply)
    • id = (known after apply)
    • insecurevalue = (known after apply)
    • keyid = (known after apply)
    • name = "example"
    • tagsall = (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:
  • ssmparamvalue = "set by terraform"
    awsssmparameter.example: Creating...
    awsssmparameter.example: Creation complete after 2s [id=example]
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
    Outputs:
    ssmparamvalue = "set by terraform"
    ```

We can see our value was set to set by terraform. We can confirm this using the AWS CLI.

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.

After the external update, terraform refresh pulls the current value of awsssmparameter.example.value from SSM. 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.

A concrete workflow described 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 awsssmparameter 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 Module for SSM Parameters

The terraform-aws-modules/ssm-parameter/aws module provides a wrapper for creating AWS SSM Parameters with less code.

Key capabilities documented:

  • 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

Example usage for a simple string parameter:

module "string" { source = "terraform-aws-modules/ssm-parameter/aws" name = "my-parameter" value = "some-value" }

SecureString example:

module "secret" { source = "terraform-aws-modules/ssm-parameter/aws" name = "my-secret-token" value = "secret123123!!!" secure_type = true }

List parameter example:

module "list" { source = "terraform-aws-modules/ssm-parameter/aws" name = "my-list-parameter" values = ["item1", "item2"] }

Ignore value changes example:

module "list" { source = "terraform-aws-modules/ssm-parameter/aws" ignore_value_changes = true name = "my-parameter-ignore-value-changes" value = "some-value" }

The locals block shows multi-parameter definitions with attributes:

locals { parameters = { "string_simple" = { value = "string_value123" } "string" = { type = "String" value = "string_value123" tier = "Intelligent-Tiering" allowed_pattern = "[a-z0-9_]+" } "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" } "list_as_autoguess_type" = { values = ["item1", "item2"] } "list_as_jsonencoded_string" = { type = "StringList" value = } } }

The impact is reduced boilerplate and consistent defaults across environments. The contextual connection is that the ignorevaluechanges flag maps directly to the lifecycle ignore_changes pattern, giving module consumers a single boolean to allow external updates without writing explicit lifecycle blocks.

CloudPosse SSM Parameter Store Module

The Terraform module for providing read and write access to the AWS SSM Parameter Store is published as cloudposse/ssm-parameter-store/aws.

Tips referenced include 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" parameter_write = [ { name = "/cp/prod/app/database/master_password" value = "password1" type = "String" overwrite = "true" description = "Production database master password" } ] tags = { ManagedBy = "Terraform" } }

Read example:

module "store_read" { source = "cloudposse/ssm-parameter-store/aws" 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 layer is stability and reproducibility. Pinning versions prevents silent breaking changes in parameter creation logic, especially for secure parameters where key_id and encryption settings matter.

Placeholder Creation Pattern for SecureString Parameters

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.

```
resource "awsssmparameter" "apikey" {
name = "/myapp/production/external
api_key"
type = "SecureString"
value = "placeholder-update-after-creation"
description = "API key for the external payment service"

lifecycle {
ignore_changes = [value]
}
}
```

Using a Custom KMS Key. By default, SecureString uses the AWS managed aws/ssm key.

The impact is that secrets can be provisioned as part of infrastructure without embedding real secrets in Terraform state or code. The placeholder is replaced by a secrets manager, Vault, or manual rotation, and Terraform will not overwrite it.

The contextual layer connects to regular rotation of credentials and security hygiene. The parameter exists in code, is discoverable, and can be referenced by other resources, while the actual secret value remains under external control.

Practical Examples and CLI Verification

The reference material shows the full create flow with plan output and apply completion messages. The ARN format shown is arn:aws:ssm:us-east-1:012345678910:parameter/example. The LastModifiedDate is 2024-09-17T08:03:23.914000+10:00. Version is 1. DataType is text. Type is String.

The output ssmparamvalue = "set by terraform" is shown both after creation and after a subsequent apply where the value has reverted to set by terraform when lifecycle is not used, and remains stable when ignore_changes is applied.

The module examples demonstrate different parameter shapes: String, SecureString, StringList, with tier options Intelligent-Tiering and Advanced, with allowedpattern constraints, with keyid c938de44-1c09-4c91-89fd-b5881f06f317 for custom KMS encryption.

Comparison of Approaches

Structured comparison of patterns documented:

Approach Module Source Parameter Name Example Value Handling External Updates Allowed
Direct resource with lifecycle terraform awsssmparameter example value = "set by terraform" with ignore_changes Yes
terraform-aws-modules/ssm-parameter terraform-aws-modules/ssm-parameter/aws my-parameter value = "some-value" Via ignorevaluechanges
terraform-aws-modules/ssm-parameter secure terraform-aws-modules/ssm-parameter/aws my-secret-token secure_type = true Via ignorevaluechanges
terraform-aws-modules/ssm-parameter list terraform-aws-modules/ssm-parameter/aws my-list-parameter values = ["item1","item2"] Configurable
cloudposse store write cloudposse/ssm-parameter-store/aws /cp/prod/app/database/master_password value = "password1", overwrite = "true" Write via module
placeholder secure awsssmparameter /myapp/production/externalapikey value = "placeholder-update-after-creation" with ignore_changes Yes

The table shows how each pattern maps to a specific use case. Direct resource with lifecycle gives fine grained control for single parameters. Module wrappers reduce repetition for multiple parameters. CloudPosse module adds read and write separation. Placeholder pattern supports secret rotation workflows.

State Synchronization and Real World Impact

When terraform refreshes its state, it pulls the current value of awsssmparameter.example.value from SSM. This means dependent resources can react to live changes without a configuration change. The impact for operations teams is that a promotion pipeline can read a SSM parameter updated by a previous stage and use it immediately.

The risk of not using ignore_changes is drift correction that overwrites external updates. The risk of not using Terraform at all is untracked resources. The documented pattern balances both.

The contextual connection to CI/CD is explicit: GitHub Actions building docker images and pushing them to ECR can update the current stable tag in SSM. Terraform can pull this value when deploying ECS task or Lambda function. This decouples build and deploy concerns while keeping infrastructure declarative.

Conclusion

Managing AWS SSM Parameters with Terraform while allowing external updates is achieved through the lifecycle meta argument with ignorechanges on the value attribute, combined with module wrappers that expose ignorevalue_changes flags and placeholder creation patterns for SecureString parameters.

The reference material demonstrates creation plans, CLI verification, module examples with string, secure, list parameters, tier and allowedpattern settings, custom KMS keyid usage, and CloudPosse read/write modules with explicit overwrite and tagging. The documented workflows show that Terraform state remains in sync with reality through refresh, outputs reflect live values, and external processes can safely maintain parameter contents without Terraform reverting them.

Regular rotation of credentials remains a security requirement. Infrastructure as code ensures consistency. Lifecycle arguments for SSM Params allow both to coexist without conflict or surprises.

Sources

  1. Managing AWS SSM Parameters with Terraform with
  2. terraform-aws-modules/terraform-aws-ssm-parameter
  3. TerraformFoundation/terraform-aws-ssm-parameter-store
  4. oneuptime.com

Related Posts