Terraform redeploying aws_instance every apply despite no changes to user_data

Hello.

We have a terraform module with a few child modules within it.
One of the child modules contains the following resource and data source:

  1. resource.aws_instance
  2. data.template_cloudinit_config
  • The resource.aws_instance is using the data.template_cloudinit_config rendered output as it’s user_data
  • The data.template_cloudinit_config is looking at a simple bash script

I only want the aws_instance to redeploy when the bash script has changed
The issue I am having is that the aws_instance is being forced to be replaced every terraform apply.
This is due to terraform re-rendering the data.template_cloudinit_config despite no changes to the bash script it is tracking.

Is there any reason why this is happening?

This only started happening when I moved these resources into a child module, originally they were in the parent module and this never happened.

Hi @MattTDickinson,

To answer this properly I’d need to see the relevant configuration and the associated output from terraform plan, but based on what you’ve shared my first guess would be that your data block depends on a resource block that has changes planned, and so Terraform must wait until the apply phase to call that data source in case the managed resource change affects the outcome. (Terraform can’t tell that template_cloudinit_config doesn’t access any network services, so it needs to be conservative.)

If the data resource gets deferred to the apply phase then its result would be unknown during planning, and so Terraform cannot determine whether it has changed compared to the previous user_data value.

If my theory is correct then the answer would be to make sure the data block doesn’t depend on something that is changing. If this is happening in every plan then that suggests that you have an incorrectly-configured resource block that isn’t converging after a single plan and apply (sometimes known as a “permadiff”, in which case I would suggest fixing that as the root cause.

If the above is not helpful, please share a real configuration example and the plan output that shows the full set of unwanted changes.

I can’t post a lot due to confidentiality

resource.aws_instance

resource "aws_instance" "red" {
  ami                         = var.ami_id
  instance_type               = var.instance_size
  iam_instance_profile        = var.instance_profile
  key_name                    = var.key_pair_name
  user_data                   = data.template_cloudinit_config.red.rendered
  user_data_replace_on_change = true

  ebs_optimized = true
  root_block_device {
    volume_type = "gp3"
  }
  volume_tags =  var.additional_resource_tags

  network_interface {
    network_interface_id = aws_network_interface.red.id
    device_index         = 0
  }

  tags = var.resource_tags

  lifecycle {
    ignore_changes = [ebs_block_device]
  }
}

data source

data "template_cloudinit_config" "red" {

  gzip          = true
  base64_encode = true

  part {
    filename     = "cloud-config.sh"
    content_type = "text/x-shellscript"
    content      = local.part1
  }

local

  part1 = templatefile("${path.module}/scripts/cloud-config.sh", {
    region               = var.region
    workflow             = var.workflow
    environment          = var.environment
  })

An example ofAn example of terraform action plan

  # module.xyz.module.xyz["xyz"].data.template_cloudinit_config.red will be read during apply
  # (config refers to values not yet known)
 <= data "template_cloudinit_config" "red"  {
      ~ id            = "1878789531" -> (known after apply)
      ~ rendered      = "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......" -> (known after apply)
        # (2 unchanged attributes hidden)

      ~ part {
            # (3 unchanged attributes hidden)
        }

    }

an example of the aws_instance apply plan

# module.xyz.module.xyz["xyz"].aws_instance.red must be replaced
-/+ resource "aws_instance" "red" {
      ~ user_data                            = "f30bd5a600a88aacf234c18f777f8be8b18dd613" -> (known after apply) # forces replacement

There’s nothing immediately obvious that the data block depends on only the terraform local ‘part1’ which is a templatefile.
we’ve tried removing all variables the template file is using and the data block is still being render each apply

@MattTDickinson, the example output shows the data source is nested in some modules – are either of those modules called with depends_on?

Yes actually @jbardin. This child module has a depends_on on another child module

main.tf :

module "configuration_step_function" {
  source                   = "./modules/step_function"
  count                    = var.deploy_step_function == true ? 1 : 0
  environment              = var.environment
  workflow                 = var.workflow
  workflow_no_id           = var.workflow_no_id
  region                   = var.region
  site                     = var.site
  deploymentid             = var.deploymentid
  mwcore_master_private_ip = var.mwcore_master_private_ip
  mwcore_wrk01_private_ip  = var.mwcore_wrk01_private_ip
  mwcore_wrk02_private_ip  = var.mwcore_wrk02_private_ip
  mwedge_license_pool      = var.mwedge_license_pool
  mwedge_license_account   = var.mwedge_license_account
  resource_bucket_name     = var.resource_bucket_name
  resource_tags            = var.resource_tags
  lambda_security_group_id = var.lambda_security_group_id
  subnet_id                = var.subnet_id
  sub_department           = var.sub_department
}

module "non_live_ec2" {
  source                     = "./modules/ec2"
  for_each                   = var.channels
  channel                    = replace(lower(each.key), " ", "-")
  subnet_id                  = var.subnet_id
  environment                = var.environment
  workflow                   = var.workflow
  workflow_no_id             = var.workflow_no_id
  region                     = var.region
  resource_tags              = var.resource_tags
  ec2_specific_tags          = var.ec2_specific_tags
  aws_account_id             = var.aws_account_id
  resource_bucket_name       = var.resource_bucket_name
  instance_size              = var.instance_sizes.non_live
  ami_id                     = var.ami_id
  instance_profile           = aws_iam_instance_profile.this.name
  route53_zone_id            = var.route53_zone_id
  site                       = var.site
  sub_department             = var.sub_department
  security_groups            = concat([aws_security_group.this.id], var.additional_security_groups)
  alarms_sns_topic_base_name = var.alarms_sns_topic_base_name
  additional_resource_tags = merge(
    { service2 = replace(lower(each.key), " ", "-") }, var.additional_resource_tags,
  var.deploy_video_monitoring == true ? { "video_testing" : "false" } : {})
  s3_endpoint_dns             = var.s3_endpoint_dns
  depends_on                  = [module.configuration_step_function]
}

at the end there

Hi @MattTDickinson,

Terraform understands what you’ve configured there as "all actions for module "non_live_ec2" must happen after all actions for module "configuration_step_function".

If there are any planned changes for the step function module then Terraform must wait until the apply phase to do any actions – including reading data sources – needed for the EC2 module.

This configuration seems a little problematic because it will – as a result of all of these smaller interactions – cause the EC2 instance to be replaced every time anything changes about the step function.

The most ideal solution here would be to remove the depends_on entirely and describe the dependencies to Terraform more precisely by passing the necessary values from output values of one module to specific input variables of the other. Terraform can then follow the fine-grain dependency relationships through the individual input variables and output values.

A potentially-less-invasive alternative would be to hoist the data resource up into the root module and pass its result into the second module. The data resource will therefore be outside the scope of the depends_on and can therefore have its own fine-grain dependency relationships despite the second module still using a coarse dependency relationship for the entire module.

@apparentlymart that’s a great explanation thank you. I’ll give that a go and update on here the result.