hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io/
Other
42.37k stars 9.49k forks source link

Plan does not show that changed sensitive value results in resource replacement #28540

Closed HannesHil closed 3 years ago

HannesHil commented 3 years ago

Terraform Version

Terraform v0.15.1
on linux_amd64
+ provider registry.terraform.io/databrickslabs/databricks v0.3.1
+ provider registry.terraform.io/hashicorp/azurerm v2.45.1
+ provider registry.terraform.io/hashicorp/local v2.0.0
+ provider registry.terraform.io/hashicorp/random v3.1.0
+ provider registry.terraform.io/hashicorp/template v2.2.0
+ provider registry.terraform.io/hashicorp/tls v3.0.0

Terraform Configuration Files

resource "azurerm_linux_virtual_machine" "vm-esdata" {
  count                 = var.es_data_vm_count
  name                  = "${var.es_data_vm_name}-${local.name_suffix}-${count.index}"
  location              = azurerm_resource_group.rgenv.location
  resource_group_name   = azurerm_resource_group.rgenv.name
  network_interface_ids = [element(azurerm_network_interface.nic-esdata.*.id, count.index)]
  size                  = var.es_data_vm_size
  admin_username        = var.admin_username

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.admin_ssh_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = var.manageddisk_type
  }

  source_image_reference {
    publisher = var.storageimage_publisher
    offer     = var.storageimage_offer
    sku       = var.storageimage_sku
    version   = var.storageimage_version
  }

  admin_password                  = var.es_vm_admin_password
  custom_data                     = data.template_cloudinit_config.es-data-config[count.index].rendered
  disable_password_authentication = false
  encryption_at_host_enabled      = false

  tags = {
    environment = var.environment
    stage       = var.stage
  }
}

Expected Behavior

  # azurerm_linux_virtual_machine.vm-esdata[1] must be replaced
-/+ resource "azurerm_linux_virtual_machine" "vm-esdata" {
      ~ computer_name                   = "vm-es-data-iot-prod-1" -> (known after apply)
      ~ custom_data                     = (sensitive value) # forces replacement
      ~ id                              = ##### -> (known after apply)
        name                            = "vm-es-data-iot-prod-1"
      ~ private_ip_address              = ##### -> (known after apply)
      ~ private_ip_addresses            = [
          - #####,
        ] -> (known after apply)
      + public_ip_address               = (known after apply)
      ~ public_ip_addresses             = [] -> (known after apply)
        tags                            = {
            "environment" = "iot"
            "stage"       = "prod"
        }
      ~ virtual_machine_id              = ##### -> (known after apply)
      + zone                            = (known after apply)
        # (13 unchanged attributes hidden)

      ~ os_disk {
          ~ disk_size_gb              = 30 -> (known after apply)
          ~ name                      = ##### -> (known after apply)
            # (3 unchanged attributes hidden)
        }

        # (2 unchanged blocks hidden)
    }

Actual Behavior

  # azurerm_linux_virtual_machine.vm-esdata[1] must be replaced
-/+ resource "azurerm_linux_virtual_machine" "vm-esdata" {
      ~ computer_name                   = "vm-es-data-iot-prod-1" -> (known after apply)
      ~ custom_data                     = (sensitive value)
      ~ id                              = ##### -> (known after apply)
        name                            = "vm-es-data-iot-prod-1"
      ~ private_ip_address              = ##### -> (known after apply)
      ~ private_ip_addresses            = [
          - #####,
        ] -> (known after apply)
      + public_ip_address               = (known after apply)
      ~ public_ip_addresses             = [] -> (known after apply)
        tags                            = {
            "environment" = "iot"
            "stage"       = "prod"
        }
      ~ virtual_machine_id              = ##### -> (known after apply)
      + zone                            = (known after apply)
        # (13 unchanged attributes hidden)

      ~ os_disk {
          ~ disk_size_gb              = 30 -> (known after apply)
          ~ name                      = ##### -> (known after apply)
            # (3 unchanged attributes hidden)
        }

        # (2 unchanged blocks hidden)
    }# forces replacement

Steps to Reproduce

  1. terraform init
  2. terraform plan

Contex

We had the issue that we could not see why terraform had to replace the VM. After testing around with the _ignorechanges setting we discovered that the custom-data had changed. Terraform should show which field forces the replacement even when it's a sensitive value.

alisdair commented 3 years ago

Hi @HannesHil, thanks for reporting this. Unfortunately I'm not able to reproduce the issue with your reproduction steps. Can you be more specific about exactly how to reproduce this?

Here's what I tried, using a random_pet resource to simplify the reproduction. Create a simple config:

variable "prefix" {
  type = string
  sensitive = true
}

resource "random_pet" "pet" {
  prefix = var.prefix
}

The result:

Terraform will perform the following actions:

  # random_pet.pet must be replaced
-/+ resource "random_pet" "pet" {
      ~ id        = "foo-inviting-airedale" -> (known after apply)
      ~ prefix    = (sensitive) # forces replacement
        # (2 unchanged attributes hidden)
    }

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

As you can see, changing the sensitive prefix attribute results in it being marked as # forces replacement as expected.

HannesHil commented 3 years ago

Hey @alisdair, thanks for your fast reply. Its true, your pet example works as i would expect. Because of your example i am not sure if its a terraform issue or maybe a provider issue.

Inspired by the example code for an azure _vm the follwing code has the missing replacement explanation. To reproduce:

  1. Just apply once(create the vm and the other resources)
  2. Than change the local script_data
  3. Plan or Apply a second time. -> instance has to be replaced but without any explanation
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=2.46.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "example" {
  name     = "example-resources"
  location = "West Europe"
}

resource "azurerm_virtual_network" "example" {
  name                = "example-network"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
}

resource "azurerm_subnet" "example" {
  name                 = "internal"
  resource_group_name  = azurerm_resource_group.example.name
  virtual_network_name = azurerm_virtual_network.example.name
  address_prefixes     = ["10.0.2.0/24"]
}

locals {
  script_data = <<CUSTOM_DATA
  #!/bin/bash
  echo "Hello World?!"
  CUSTOM_DATA
  }

resource "azurerm_network_interface" "example" {
  name                = "example-nic"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.example.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_linux_virtual_machine" "testvm" {
  name                  = "testvm"
  resource_group_name   = azurerm_resource_group.example.name
  location              = azurerm_resource_group.example.location
  size                = "Standard_F2"
  admin_username      = "adminuser"
  network_interface_ids = [
    azurerm_network_interface.example.id,
  ]

  admin_ssh_key {
    username   = "adminuser"
    public_key = file("~/.ssh/id_rsa.pub")
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "16.04-LTS"
    version   = "latest"
  }

  custom_data                     = base64encode(local.script_data)
}

Hope this helps!

alisdair commented 3 years ago

Thanks so much! This is a perfect reproduction and I can see now that it's a simple bug in the plan renderer. Sensitive attributes (as marked by the provider) are missing a check for the "forces replacement" output, whereas sensitive values (as defined by user configuration) are working correctly. I'll have a fix in a PR on Monday morning.

alisdair commented 3 years ago

The fix is approved and will go out with 0.15.2. Thanks again for the report!

HannesHil commented 3 years ago

it was a pleasure. Looking forward to the next version :)

github-actions[bot] commented 3 years ago

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.