Managing AWS SSM Parameters with Terraform with External Updates
Learn to manage AWS SSM Parameters with Terraform while allowing external updates seamlessly!
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 (SSM) Parameters when parameters are modified by other applications or workflows. In this article, we'll explore how to use Terraform to create and manage SSM Parameters while allowing external updates. This ensures your infrastructure is tracked while your Terraform state stays in sync with reality. We'll demonstrate how to use Terraform's lifecycle
meta argument and demonstrate how it can be used to solve this challenge.
The Challenge: SSM Parameters Updated Outside Terraform
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 Solution: Terraform's lifecycle
Meta Argument
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 "aws_ssm_parameter" "example" {
name = "example"
type = "String"
value = "set by terraform"
lifecycle {
ignore_changes = [
value,
]
}
}
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. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_ssm_parameter.example will be created
+ 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"
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"
dave@laptop:/home/dave/terraform/examples/ssm$
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"
}
}
dave@laptop:/home/dave/terraform/examples/ssm$
To simulate our value being updated by another process, we can update the value using the AWS CLI. It could be a Lambda function, Step Function or an application updating this value.
dave@laptop:/home/dave/terraform/examples/ssm$ aws ssm put-parameter --name example --overwrite --value "set by cli"
{
"Version": 2,
"Tier": "Standard"
}
dave@laptop:/home/dave/terraform/examples/ssm$ # Let's check that the value updated properly
dave@laptop:/home/dave/terraform/examples/ssm$ aws ssm get-parameter --name example
{
"Parameter": {
"Name": "example",
"Type": "String",
"Value": "set by cli",
"Version": 2,
"LastModifiedDate": "2024-09-17T08:03:38.398000+10:00",
"ARN": "arn:aws:ssm:us-east-1:012345678910:parameter/example",
"DataType": "text"
}
}
dave@laptop:/home/dave/terraform/examples/ssm$
Everything is working as expected. The value is now set by cli
. If we didn’t use the lifecycle
meta argument our changes would be lost when we run terraform apply
. We will run it and set the manually updated value it retained.
dave@laptop:/home/dave/terraform/examples/ssm$ terraform apply -auto-approve
aws_ssm_parameter.example: Refreshing state... [id=example]
Note: Objects have changed outside of Terraform
Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan:
# aws_ssm_parameter.example has changed
~ resource "aws_ssm_parameter" "example" {
id = "example"
name = "example"
~ value = (sensitive value)
# (9 unchanged attributes hidden)
}
Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Changes to Outputs:
~ ssm_param_value = "set by terraform" -> "set by cli"
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
ssm_param_value = "set by cli"
dave@laptop:/home/dave/terraform/examples/ssm$
As we can see, the value remains unchanged. If we remove the lifecycle
argument, we will see the value reverts. Here is the updated main.tf
.
resource "aws_ssm_parameter" "example" {
name = "example"
type = "String"
value = "set by terraform"
# lifecycle {
# ignore_changes = [
# value,
# ]
#}
}
output "ssm_param_value" {
# This is a bad idea. I'm doing it to show the value after each update
value = nonsensitive(aws_ssm_parameter.example.value)
}
Now when we run terraform apply
our value will be reverted.
dave@laptop:/home/dave/terraform/examples/ssm$ terraform apply -auto-approve
aws_ssm_parameter.example: Refreshing state... [id=example]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_ssm_parameter.example will be updated in-place
~ resource "aws_ssm_parameter" "example" {
id = "example"
+ insecure_value = (known after apply)
name = "example"
tags = {}
~ value = (sensitive value)
~ version = 2 -> (known after apply)
# (8 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Changes to Outputs:
~ ssm_param_value = "set by cli" -> "set by terraform"
aws_ssm_parameter.example: Modifying... [id=example]
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
.
Conclusion
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.
We could have 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.
Need Help?
If you want to adopt Proactive Ops, but you're not sure where to start, get in touch! I am happy to help get you.
Proactive Ops is produced on the unceeded territory of the Ngunnawal people. We acknowledge the Traditional Owners and pay respect to Elders past and present.