Lifecycle and the existing resources

Hello,
I have an existing resource (aws_secretsmanager_secret_version) that over period of time changed ( new secret values).
I added

lifecycle {
   ignore_changes = [secret_string]
}

to the code, but TF plans to recreate the resource.

Question:
how can i avoid recreating the resource and add the “lifecycle” so that next time the resource do not get recreated?

Hi @AZZ,

I think w’ll need a little more information to understand what is happening here.
If the only change is to the secret_string attribute via the configuration, then the ignore_changes additional you have should work. Can you show what the configuration and plan output looks like?

@jbardin ,
thank you for looking. I bit the bullet and ran the code, since i have to move forward. As a result, the secret (aws_secretsmanager_secret_version.rds_secret_version[“rds01”]) was recreated with the new (default) password (secret_string-> aws_db_instance.dbs[each.key].password).
I manually restored the password value.

Here is the code with lifecycle added . Below you can find the plan it generated.

P.S. TF updates the secret because i also added the ‘description’. Without that - it was omitting secret resource update ( as expected ).

resource "aws_secretsmanager_secret" "rds_secret" {
  for_each                = var.RDS
  name                    = lower("${each.value.tag_productfamily}_${each.value.identifier}_rds_secret")
  description             = "The secret for RDS ${aws_db_instance.dbs[each.key].address}"
  recovery_window_in_days = 0
  tags = {
    Product = aws_db_instance.dbs[each.key].tags_all["Productfamily"]
    Group   = aws_db_instance.dbs[each.key].tags_all["Group"]
  }
}

resource "aws_secretsmanager_secret_version" "rds_secret_version" {
  for_each = var.RDS
  lifecycle {
    ignore_changes = [secret_string]
  }
  secret_id = aws_secretsmanager_secret.rds_secret[each.key].id
  secret_string = jsonencode({
    username             = each.value.username
    password             = aws_db_instance.dbs[each.key].password
    engine               = each.value.engine
    host                 = split(":", aws_db_instance.dbs[each.key].endpoint)[0]
    port                 = aws_db_instance.dbs[each.key].port
    dbInstanceIdentifier = aws_db_instance.dbs[each.key].identifier
  })
}

Here is the complete log, with a light editing:

data.aws_security_group.vpc_sec_grp["sg_NonProd_RDS"]: Reading...
aws_db_subnet_group.db_subnet_group_az12["rds01"]: Refreshing state... [id=d9_nonprod_subgrp]
data.aws_security_group.vpc_sec_grp["sg_NonProd_RDS"]: Read complete after 0s [id=sg-01a77fa8b2238e527]
aws_db_instance.dbs["rds01"]: Refreshing state... [id=db-Y7ZCWJ2R4B3YKHKF6KTMP3LZNU]
aws_secretsmanager_secret.rds_secret["rds01"]: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7]
aws_secretsmanager_secret_version.rds_secret_version["rds01"]: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7|C1E8AE93-EA46-4A58-9D46-5D5C7B6C440D]
aws_secretsmanager_secret_rotation.rds_secret_rotation["rds01"]: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
 <= read (data resources)

Terraform will perform the following actions:

  # data.aws_lambda_function.rotate["rds01"] will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "aws_lambda_function" "rotate" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_signing_config_arn        = (known after apply)
      + dead_letter_config             = (known after apply)
      + description                    = (known after apply)
      + environment                    = (known after apply)
      + ephemeral_storage              = (known after apply)
      + file_system_config             = (known after apply)
      + function_name                  = "RDSPassRotate"
      + handler                        = (known after apply)
      + id                             = (known after apply)
      + image_uri                      = (known after apply)
      + invoke_arn                     = (known after apply)
      + kms_key_arn                    = (known after apply)
      + last_modified                  = (known after apply)
      + layers                         = (known after apply)
      + logging_config                 = (known after apply)
      + memory_size                    = (known after apply)
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = (known after apply)
      + role                           = (known after apply)
      + runtime                        = (known after apply)
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + source_code_hash               = (known after apply)
      + source_code_size               = (known after apply)
      + tags                           = (known after apply)
      + timeout                        = (known after apply)
      + tracing_config                 = (known after apply)
      + version                        = (known after apply)
      + vpc_config                     = (known after apply)
    }

  # aws_secretsmanager_secret.rds_secret["rds01"] will be updated in-place
  ~ resource "aws_secretsmanager_secret" "rds_secret" {
      + description                    = "The secret for RDS d9-nonprod.XXXXXXXXXXXX.us-east-1.rds.amazonaws.com"
        id                             = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7"
        name                           = "d9_nonprod_rds_secret"
        tags                           = {
            "Group"   = "NonProd"
            "Product" = "D9"
        }
        # (4 unchanged attributes hidden)
    }

  # aws_secretsmanager_secret_rotation.rds_secret_rotation["rds01"] will be updated in-place
  ~ resource "aws_secretsmanager_secret_rotation" "rds_secret_rotation" {
        id                  = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7"
      ~ rotation_lambda_arn = "arn:aws:lambda:us-east-1:XXXXXXX7:function:RDSPassRotate" -> (known after apply)
        # (3 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # aws_secretsmanager_secret_version.rds_secret_version["rds01"] will be created
  + resource "aws_secretsmanager_secret_version" "rds_secret_version" {
      + arn            = (known after apply)
      + id             = (known after apply)
      + secret_id      = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7"
      + secret_string  = (sensitive value)
      + version_id     = (known after apply)
      + version_stages = (known after apply)
    }

Plan: 1 to add, 2 to change, 0 to destroy.

Thanks @AZZ, so from what I see it worked as expected now?

There is no replacement change for any existing aws_secretsmanager_secret_version instances (either they did not exist, or ignore_changes suppressed any change), and only the creation of a new instance with the "rds01" key.

yes, it seems like it, but the object did exist prior to the run. the only difference was the password…

your comment made me doubt the recollation of the events, so i scrolled back thru the logs. Luckely I found everything I did. Here we go.
Originally ( first time last week i tried to refresh the code) these are the logs :

2024-04-12T13:22:59-04:00 INF running cmd="terraform apply" stack=/Clients/SOPRO/D9/common/rds
data.aws_security_group.vpc_sec_grp["sg_NonProd_RDS"]: Reading...
aws_db_subnet_group.db_subnet_group_az12["rds01"]: Refreshing state... [id=d9_nonprod_subgrp]
data.aws_security_group.vpc_sec_grp["sg_NonProd_RDS"]: Read complete after 0s [id=sg-01a77fa8b2238e527]
aws_db_instance.dbs["rds01"]: Refreshing state... [id=db-Y7ZCWJ2R4B3YKHKF6KTMP3LZNU]
aws_secretsmanager_secret.rds_secret["rds01"]: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7]
aws_secretsmanager_secret_version.rds_secret_version["rds01"]: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7|C1E8AE93-EA46-4A58-9D46-5D5C7B6C440D]
aws_secretsmanager_secret_rotation.rds_secret_rotation["rds01"]: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
 <= read (data resources)

Terraform will perform the following actions:

  # data.aws_lambda_function.rotate["rds01"] will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "aws_lambda_function" "rotate" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_signing_config_arn        = (known after apply)
      + dead_letter_config             = (known after apply)
      + description                    = (known after apply)
      + environment                    = (known after apply)
      + ephemeral_storage              = (known after apply)
      + file_system_config             = (known after apply)
      + function_name                  = "RDSPassRotate"
      + handler                        = (known after apply)
      + id                             = (known after apply)
      + image_uri                      = (known after apply)
      + invoke_arn                     = (known after apply)
      + kms_key_arn                    = (known after apply)
      + last_modified                  = (known after apply)
      + layers                         = (known after apply)
      + logging_config                 = (known after apply)
      + memory_size                    = (known after apply)
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = (known after apply)
      + role                           = (known after apply)
      + runtime                        = (known after apply)
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + source_code_hash               = (known after apply)
      + source_code_size               = (known after apply)
      + tags                           = (known after apply)
      + timeout                        = (known after apply)
      + tracing_config                 = (known after apply)
      + version                        = (known after apply)
      + vpc_config                     = (known after apply)
    }

  # aws_secretsmanager_secret.rds_secret["rds01"] will be updated in-place
  ~ resource "aws_secretsmanager_secret" "rds_secret" {
        id                             = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7"
        name                           = "d9_nonprod_rds_secret"
      ~ tags                           = {
            "Group"   = "NonProd"
          - "Product" = "D9" -> null
        }
      ~ tags_all                       = {
          - "Product"               = "D9" -> null
            # (8 unchanged elements hidden)
        }
        # (3 unchanged attributes hidden)
    }

  # aws_secretsmanager_secret_rotation.rds_secret_rotation["rds01"] will be updated in-place
  ~ resource "aws_secretsmanager_secret_rotation" "rds_secret_rotation" {
        id                  = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7"
      ~ rotation_lambda_arn = "arn:aws:lambda:us-east-1:XXXXXXX7:function:RDSPassRotate" -> (known after apply)
        # (3 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # aws_secretsmanager_secret_version.rds_secret_version["rds01"] will be created
  + resource "aws_secretsmanager_secret_version" "rds_secret_version" {
      + arn            = (known after apply)
      + id             = (known after apply)
      + secret_id      = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7"
      + secret_string  = (sensitive value)
      + version_id     = (known after apply)
      + version_stages = (known after apply)
    }

Plan: 1 to add, 2 to change, 0 to destroy.

Do you want to perform these actions in workspace "ESRAWEB_D9-COMMON-RDS"?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: n

Apply cancelled.



terraform state list
data.aws_lambda_function.rotate["rds01"]
data.aws_security_group.vpc_sec_grp["sg_NonProd_RDS"]
aws_db_instance.dbs["rds01"]
aws_db_subnet_group.db_subnet_group_az12["rds01"]
aws_secretsmanager_secret.rds_secret["rds01"]
aws_secretsmanager_secret_rotation.rds_secret_rotation["rds01"]
aws_secretsmanager_secret_version.rds_secret_version["rds01"]

terraform state show aws_secretsmanager_secret_version.rds_secret_version[\"rds01\"]
# aws_secretsmanager_secret_version.rds_secret_version["rds01"]:
resource "aws_secretsmanager_secret_version" "rds_secret_version" {
    arn            = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7"
    id             = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7|C1E8AE93-EA46-4A58-9D46-5D5C7B6C440D"
    secret_id      = "arn:aws:secretsmanager:us-east-1:XXXXXXX7:secret:d9_nonprod_rds_secret-qBucb7"
    secret_string  = (sensitive value)
    version_id     = "C1E8AE93-EA46-4A58-9D46-5D5C7B6C440D"
    version_stages = [
        "AWSPREVIOUS",
    ]
}

I’m not certain why the rds rotation function needs to be updated. Nothing changed there, based on my examination of the resources, nor it changed after i reran the code earleir today…

The data source needs to be deferred until apply because it says it depends on a resource or a module with changes pending, which usually means an explicit depends_on was added to the configuration, though it could be because it is directly referencing a managed resource which contains a change (I would need to see the config to be sure).

If the aws_secretsmanager_secret_version.rds_secret_version["rds01"] is in the saved state but the next plan shows it being created, then that would mean the provider indicated that the instance no longer exists during refresh, so it ends up as a create action instead of update/replace.

yes, the function has the explicit ‘depends_on’:

data "aws_lambda_function" "rotate" {
  for_each      = var.RDS
  function_name = each.value.rotate_func
depends_on    = [aws_secretsmanager_secret_version.rds_secret_version]
}

and it explains perfectly well why it offered to update the function. Thank you. I overlooked that.

As for the secret_version:
I checked the resource in the aws console and the state had up to date version of it. Well, minus “Secret” of course.

The question is not why the resource version was recreated, but how to apply the lifecycle ( or any other method) so that the resource does not get recreated.

The ignore_changes is what you want, and with a simple attribute like secret_string will typically avoid the unwanted replacement. The plan you are showing however is not replacing the aws_secretsmanager_secret_version, from Terraform’s perspective it is creating the "rds01" instance because there is no corresponding instance, so there are no changes to ignore.

The only explanation I have for that is the resource was removed externally from Terraform. If that’s the case running a plan/apply with -refresh-only would update the current state to match showing that the instance is indeed missing.

Thank you. The explanation makes sense. Unfortunately i can’t validate your explanation, since i already created the resource with the ignore.
I don’t expect the resource to produce a new update plan once the secret version changes in a few hours. Yet, i’ll check it, just to be sure. I’ll update the thread if something unexpected pops up.
Thank you @jbardin !