2019-05-07

Terraform is Terrible: Part 2

There's such a thing as being too careful. I know this because as I read the Terraform documentation, the first examples all said "in main.tf, define your resources", so I did, just like they told me to do:

resource "azurerm_virtual_machine" "helloterraformvm" {
    name = "terraformvm"
    location = "West US"
    resource_group_name = "${azurerm_resource_group.helloterraform.name}"
    network_interface_ids = ["${azurerm_network_interface.helloterraformnic.id}"]
    vm_size = "Standard_A0"

    storage_image_reference {
        publisher = "Canonical"
        offer = "UbuntuServer"
        sku = "14.04.2-LTS"
        version = "latest"
    }

    storage_os_disk {
        name = "myosdisk"
        vhd_uri = "${azurerm_storage_account.helloterraformstorage.primary_blob_endpoint}${azurerm_storage_container.helloterraformstoragestoragecontainer.name}/myosdisk.vhd"
        caching = "ReadWrite"
        create_option = "FromImage"
    }

    os_profile {
        computer_name = "hostname"
        admin_username = "testadmin"
        admin_password = "Password1234!"
    }

    os_profile_linux_config {
        disable_password_authentication = false
    }
}

Looks good, right? It's lengthy, but it's clean. Maybe I should have just started there. But I kept reading and at one point the Terraform documentation introduces modules and here's where everything goes off the rails.

The Terraform docs state, and I quote:

Building configurations one at a time works well for learning and ad hoc testing, but it does not scale.... Lack of consistency and reusability will lead to management problems, and complicate troubleshooting.

So I was thusly terrorized into throwing out my test main.tf right away and I switched to using modules. "Pffft!" I thought. "Why even bother starting with resource blocks if modules are so much better?"

What a fool I was.

A module, it turns out, is just a wrapper around a resource block that someone you don't know and will never meet wrote to define wrong values to use for defaults and you're better off writing out your resource definitions the long way. I switched to a module and, yes, it was much shorter and cleaner looking. It's also broken:

module "compute" {
    source            = "Azure/compute/azurerm"
    version           = "1.2.0"
    location          = "${var.location}"
    vnet_subnet_id    = "${element(module.network.vnet_subnets, 0)}"
    admin_username    = "plankton"
    admin_password    = "Password1234!"
    remote_port       = "22"
    vm_os_simple      = "UbuntuServer"
    vm_size           = "${lookup(var.vm_size, var.environment)}"
    public_ip_dns     = ["zzdns"]
}

That looks nice! I'm calling a "compute" module, I'm defining the source as "Azure/compute/azurerm", and then the rest of the attributes are generic and platform-independent. Whether I run this on Azure or AWS or Uncle Joe's Server Shack, there are only a couple of tweaks I need to make. More importantly, I can come back to this block of config info in six months and understand it without needing to put on a pair of reading glasses and a pot of coffee.

But I didn't want to hardcode a username and password in my main.tf, so I did what the documentation said to do and I exported those values to a terraform.tfvars file for security purposes. A .tfvars file is really concise:

location = "East US 2"
username = "plankton"
password = "Password1234!"

Then just substitute these values in main.tf with references to their vars:

admin_username    = "${var.username}"
admin_password    = "${var.password}"

This is gonna be easy! I ran "terraform init" and "terraform plan" to get started and the whole thing conked out and died on syntax errors.

So I kept reading. The terraform.tfvars file isn't enough. I needed a variables.tf file, too, to define the variables named "location", "username", and "password". All the examples use variables.tf and a much uglier syntax:

variable "location" {
  default = "East US 2"
}

variable "username" {
  default = "plankton"
}

variable "password" {
  default = "Password1234!"
}

Now you can see why I wanted to use the .tfvars file instead. But what the docs don't spell out very clearly is that the variables are defined in variables.tf, and then the values of those variables are defined in terraform.tfvars. So that means doing this in your variables.tf file:

variable "location" {}
variable "username" {}
variable "password" {}

So, really, a terraform.tfvars file is completely redundant because you still need to define variables in variables.tf. What's the point of having variables in one file and values in another? I can't think of a good reason except for making your deployments even more complicated, which is the opposite of what I thought Terraform was doing.

What a fool I was.

I threw out the terraform.tfvars and went with a proper variables.tf file that had my username and password in it. I still hadn't gotten a working deployment, but I blamed this syntax snafu on my limited experience and an insufficiently thorough reading of the docs. "terraform init" and "terraform plan" again and we're off to the races... right?

What I did not understand was that the examples I was reading to create a VM don't actually make VMs anymore. You'd think I wouldn't have trouble making a Microsoft Windows VM on Microsoft Azure, but the Azure/compute/azurerm module I was told to use assumes every Azure VM created will be a Linux VM and thusly errors out if the file C:\Users\me\.ssh\id_rsa.pub doesn't exist.

You can see where this is going. Like a jerk, I had changed vm_os_simple = "UbuntuServer" to vm_os_simple = "WindowsServer" and This. Broke. Everything.

I was told by the documentation to use modules in my Terraform configs. I was even told on the modules page the Terraform Registry includes a directory of "ready-to-use Azure RM modules for various common purposes". We see now that this isn't just a misstatement or exaggeration. It is an outright lie.

Next time: Nobody's default but my own.

No comments: