"Seca" by "Olearys" on Flickr

Getting Started with Terraform on Azure

I’m strongly in the “Ansible is my tool, what needs fixing” camp, when it comes to Infrastructure as Code (IaC) but, I know there are other tools out there which are equally as good. I’ve been strongly advised to take a look at Terraform from HashiCorp. I’m most familiar at the moment with Azure, so this is going to be based around resources available on Azure.


Late edit: I want to credit my colleague, Pete, for his help getting started with this. While many of the code samples have been changed from what he provided me with, if it hadn’t been for these code samples in the first place, I’d never have got started!

Late edit 2: This post was initially based on Terraform 0.11, and I was prompted by another colleague, Jon, that the available documentation still follows the 0.11 layout. 0.12 was released in May, and changes how variables are reused in the code. This post now *should* follow the 0.12 conventions, but if you spot something where it doesn’t, check out this post from the Terraform team.


As with most things, there’s a learning curve, and I struggled to find a “simple” getting started guide for Terraform. I’m sure this is a failing on my part, but I thought it wouldn’t hurt to put something out there for others to pick up and see if it helps someone else (and, if that “someone else” is you, please let me know in the comments!)

Pre-requisites

You need an Azure account for this. This part is very far outside my spectrum of influence, but I’m assuming you’ve got one. If not, look at something like Digital Ocean, AWS or VMWare :) For my “controller”, I’m using Windows Subsystem for Linux (WSL), and wrote the following notes about getting my pre-requisites.

Pre-requsites

mkdir -p ~/bin
cd ~/bin
sudo apt update && sudo apt install unzip

Install AzureCLI

Source

curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc.gpg > /dev/null
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/azure-cli.list > /dev/null
sudo apt update && sudo apt install azure-cli

Install Kubectl in WSL

Source

curl -sLO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && chmod 755 kubectl

Install Terraform in WSL

Source

curl -sLO $(curl https://www.terraform.io/downloads.html | grep "linux_amd64.zip" | cut -d\" -f 2) && unzip terraform*.zip && rm terraform*.zip && chmod 755 terraform

Install terraform extension for VSCode

https://marketplace.visualstudio.com/items?itemName=mauve.terraform

Define bash as Default VSCode Shell

Source

  1. ctrl+shift+p (Command palete)
  2. Type: default shell and select Terminal: Select Default Shell
  3. Choose "WSL Bash"

Building the file structure

One quirk with Terraform, versus other tools like Ansible, is that when you run one of the terraform commands (like terraform init, terraform plan or terraform apply), it reads the entire content of any file suffixed “tf” in that directory, so if you don’t want a file to be loaded, you need to either move it out of the directory, comment it out, or rename it so it doesn’t end .tf. By convention, you normally have three “standard” files in a terraform directory – main.tf, variables.tf and output.tf, but logically speaking, you could have everything in a single file, or each instruction in it’s own file. Because this is a relatively simple script, I’ll use this standard layout.

The actions I’ll be performing are the “standard” steps you’d perform in Azure to build a single Infrastructure as a Service (IAAS) server service:

  • Create your Resource Group (RG)
  • Create a Virtual Network (VNET)
  • Create a Subnet
  • Create a Security Group (SG) and rules
  • Create a Public IP address (PubIP) with a DNS name associated to that IP.
  • Create a Network Interface (NIC)
  • Create a Virtual Machine (VM), supplying a username and password, the size of disks and VM instance, and any post-provisioning instructions (yep, I’m using Ansible for that :) ).

I’m using Visual Studio Code, but almost any IDE will have integrations for Terraform. The main thing I’m using it for is auto-completion of resource, data and output types, also the fact that control+clicking resource types opens your browser to the documentation page on terraform.io.

So, creating my main.tf, I start by telling it that I’m working with the Terraform AzureRM Provider (the bit of code that can talk Azure API).

provider "azurerm" {
}
view raw provider.tf hosted with ❤ by GitHub

This simple statement is enough to get Terraform to load the AzureRM, but it still doesn’t tell Terraform how to get access to the Azure account. Use az login from a WSL shell session to authenticate.

Next, we create our basic resource, vnet and subnet resources.

resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_virtual_network" "vnet" {
name = var.vnet_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
address_space = ["10.0.0.0/16"]
dns_servers = ["8.8.8.8", "8.8.4.4"]
}
resource "azurerm_subnet" "subnet" {
name = var.subnet_name
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefix = "10.0.1.0/24"
}
view raw infra.tf hosted with ❤ by GitHub

But wait, I hear you cry, what are those var.something bits in there? I mentioned before that in the “standard” set of files is a “variables.tf” file. In here, you specify values for later consumption. I have recorded variables for the resource group name and location, as well as the VNet name and subnet name. Let’s add those into variables.tf.

variable resource_group_name {
default = "MFIOT201906"
}
variable vnet_name {
default = "MFIOT201906_vnet"
}
variable subnet_name {
default = "MFIOT201906_subnet"
}
variable location {
default = "UK South"
}
view raw variables.tf hosted with ❤ by GitHub

When you’ve specified a resource, you can capture any of the results from that resource to use later – either in the main.tf or in the output.tf files. By creating the resource group (called “rg” here, but you can call it anything from “demo” to “myfirstresourcegroup”), we can consume the name or location with azurerm_resource_group.rg.name and azurerm_resource_group.rg.location, and so on. In the above code, we use the VNet name in the subnet, and so on.

After the subnet is created, we can start adding the VM specific parts – a security group (with rules), a public IP (with DNS name) and a network interface. I’ll create the VM itself later. So, let’s do this.

resource "azurerm_network_security_group" "iaasnsg" {
name = "iaas-nsg"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_network_security_rule" "iaasnsgr" {
name = "iaas-nsg-100"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "${trimspace(data.http.icanhazip.body)}/32"
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.rg.name
network_security_group_name = azurerm_network_security_group.iaasnsg.name
}
resource "azurerm_public_ip" "iaaspubip" {
name = "iaas-pubip"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Dynamic"
domain_name_label = var.dns_prefix
}
resource "azurerm_network_interface" "iaasnic" {
name = "iaas-nic"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_security_group_id = azurerm_network_security_group.iaasnsg.id
ip_configuration {
name = "iaas-nic-ip"
subnet_id = azurerm_subnet.subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.iaaspubip.id
}
}
view raw network.tf hosted with ❤ by GitHub

BUT WAIT, what’s that ${trimspace(data.http.icanhazip.body)}/32 bit there?? Any resources we want to load from the terraform state, but that we’ve not directly defined ourselves needs to come from somewhere. These items are classed as “data” – that is, we want to know what their values are, but we aren’t *changing* the service to get it. You can also use this to import other resource items, perhaps a virtual network that is created by another team, or perhaps your account doesn’t have the rights to create a resource group. I’ll include a commented out data block in the overall main.tf file for review that specifies a VNet if you want to see how that works.

In this case, I want to put the public IP address I’m coming from into the NSG Rule, so I can get access to the VM, without opening it up to *everyone*. I’m not that sure that my IP address won’t change between one run and the next, so I’m using the icanhazip.com service to determine my IP address. But I’ve not defined how to get that resource yet. Let’s add it to the main.tf for now.

data "http" "icanhazip" {
url = "http://ipv4.icanhazip.com"
}
view raw ipv4.tf hosted with ❤ by GitHub

So, we’re now ready to create our virtual machine. It’s quite a long block, but I’ll pull certain elements apart once I’ve pasted this block in.

resource "azurerm_virtual_machine" "main" {
name = "iaas-vm"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_ids = [azurerm_network_interface.iaasnic.id]
vm_size = "Standard_DS1_v2"
delete_os_disk_on_termination = true
delete_data_disks_on_termination = true
storage_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
storage_os_disk {
name = "iaas-os-disk"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
os_profile {
computer_name = "iaas"
admin_username = var.ssh_user
admin_password = var.ssh_password
}
os_profile_linux_config {
disable_password_authentication = false
}
provisioner "remote-exec" {
inline = ["mkdir /tmp/ansible"]
connection {
type = "ssh"
host = azurerm_public_ip.iaaspubip.fqdn
user = var.ssh_user
password = var.ssh_password
}
}
provisioner "file" {
source = "ansible/"
destination = "/tmp/ansible"
connection {
type = "ssh"
host = azurerm_public_ip.iaaspubip.fqdn
user = var.ssh_user
password = var.ssh_password
}
}
provisioner "remote-exec" {
inline = [
"sudo apt update > /tmp/apt_update || cat /tmp/apt_update",
"sudo apt install -y python3-pip > /tmp/apt_install_python3_pip || cat /tmp/apt_install_python3_pip",
"sudo -H pip3 install ansible > /tmp/pip_install_ansible || cat /tmp/pip_install_ansible",
"ansible-playbook /tmp/ansible/main.yml"
]
connection {
type = "ssh"
host = azurerm_public_ip.iaaspubip.fqdn
user = var.ssh_user
password = var.ssh_password
}
}
}
view raw vm.tf hosted with ❤ by GitHub

So, this is broken into four main pieces.

  • Virtual Machine Details. This part is relatively sensible. Name RG, location, NIC, Size and what happens to the disks when the machine powers on. OK.
name                             = "iaas-vm"
location                         = azurerm_resource_group.rg.location
resource_group_name              = azurerm_resource_group.rg.name
network_interface_ids            = [azurerm_network_interface.iaasnic.id]
vm_size                          = "Standard_DS1_v2"
delete_os_disk_on_termination    = true
delete_data_disks_on_termination = true
  • Disk details.
storage_image_reference {
  publisher = "Canonical"
  offer     = "UbuntuServer"
  sku       = "18.04-LTS"
  version   = "latest"
}
storage_os_disk {
  name              = "iaas-os-disk"
  caching           = "ReadWrite"
  create_option     = "FromImage"
  managed_disk_type = "Standard_LRS"
}
  • OS basics: VM Hostname, username of the first user, and it’s password. Note, if you want to use an SSH key, this must be stored for Terraform to use without passphrase. If you mention an SSH key here, as well as a password, this can cause all sorts of connection issues, so pick one or the other.
os_profile {
  computer_name  = "iaas"
  admin_username = var.ssh_user
  admin_password = var.ssh_password
}
os_profile_linux_config {
  disable_password_authentication = false
}
  • And lastly, provisioning. I want to use Ansible for my provisioning. In this example, I have a basic playbook stored locally on my Terraform host, which I transfer to the VM, install Ansible via pip, and then execute ansible-playbook against the file I uploaded. This could just as easily be a git repo to clone or a shell script to copy in, but this is a “simple” example.
provisioner "remote-exec" {
  inline = ["mkdir /tmp/ansible"]

  connection {
    type     = "ssh"
    host     = azurerm_public_ip.iaaspubip.fqdn
    user     = var.ssh_user
    password = var.ssh_password
  }
}

provisioner "file" {
  source = "ansible/"
  destination = "/tmp/ansible"

  connection {
    type     = "ssh"
    host     = azurerm_public_ip.iaaspubip.fqdn
    user     = var.ssh_user
    password = var.ssh_password
  }
}

provisioner "remote-exec" {
  inline = [
    "sudo apt update > /tmp/apt_update || cat /tmp/apt_update",
    "sudo apt install -y python3-pip > /tmp/apt_install_python3_pip || cat /tmp/apt_install_python3_pip",
    "sudo -H pip3 install ansible > /tmp/pip_install_ansible || cat /tmp/pip_install_ansible",
    "ansible-playbook /tmp/ansible/main.yml"
  ]

  connection {
    type     = "ssh"
    host     = azurerm_public_ip.iaaspubip.fqdn
    user     = var.ssh_user
    password = var.ssh_password
  }
}

This part of code is done in three parts – create upload path, copy the files in, and then execute it. If you don’t create the upload path, it’ll upload just the first file it comes to into the path specified.

Each remote-exec and file provisioner statement must include the hostname, username and either the password, or SSH private key. In this example, I provide just the password.

So, having created all this lot, you need to execute the terraform workload. Initially you do terraform init. This downloads all the provisioners and puts them into the same tree as these .tf files are stored in. It also resets the state of the terraform discovered or created datastore.

Next, you do terraform plan -out tfout. Technically, the tfout part can be any filename, but having something like tfout marks it as clearly part of Terraform. This creates the tfout file with the current state, and whatever needs to change in the Terraform state file on it’s next run. Typically, if you don’t use a tfout file within about 20 minutes, it’s probably worth removing it.

Finally, once you’ve run your plan stage, now you need to apply it. In this case you execute terraform apply tfout. This tfout is the same filename you specified in terraform plan. If you don’t include -out tfout on your plan (or even run a plan!) and tfout in your apply, then you can skip the terraform plan stage entirely.

When I ran this, with a handful of changes to the variable files, I got this result:

$ terraform init
Initializing the backend...
Initializing provider plugins...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.azurerm: version = "~> 1.30"
* provider.http: version = "~> 1.1"
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
view raw terraform_init hosted with ❤ by GitHub
$ terraform plan -out tfout
Refreshing Terraform state in-memory prior to plan... [314/486]
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
data.http.icanhazip: Refreshing state...
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# azurerm_network_interface.iaasnic will be created
+ resource "azurerm_network_interface" "iaasnic" {
+ applied_dns_servers = (known after apply)
+ dns_servers = (known after apply)
+ enable_accelerated_networking = false
+ enable_ip_forwarding = false
+ id = (known after apply)
+ internal_dns_name_label = (known after apply)
+ internal_fqdn = (known after apply)
+ location = "uksouth"
+ mac_address = (known after apply)
+ name = "iaas-nic"
+ network_security_group_id = (known after apply)
+ private_ip_address = (known after apply)
+ private_ip_addresses = (known after apply)
+ resource_group_name = "20190611_JS_RG"
+ tags = (known after apply)
+ virtual_machine_id = (known after apply)
+ ip_configuration {
+ application_gateway_backend_address_pools_ids = (known after apply)
+ application_security_group_ids = (known after apply)
+ load_balancer_backend_address_pools_ids = (known after apply)
+ load_balancer_inbound_nat_rules_ids = (known after apply)
+ name = "iaas-nic-ip"
+ primary = (known after apply)
+ private_ip_address_allocation = "dynamic"
+ private_ip_address_version = "IPv4"
+ public_ip_address_id = (known after apply)
+ subnet_id = (known after apply)
}
}
# azurerm_network_security_group.iaasnsg will be created
+ resource "azurerm_network_security_group" "iaasnsg" {
+ id = (known after apply)
+ location = "uksouth"
+ name = "iaas-nsg"
+ resource_group_name = "20190611_JS_RG"
+ security_rule = (known after apply)
+ tags = (known after apply)
}
# azurerm_network_security_rule.iaasnsgr will be created
+ resource "azurerm_network_security_rule" "iaasnsgr" {
+ access = "Allow"
+ destination_address_prefix = "*"
+ destination_port_range = "22"
+ direction = "Inbound"
+ id = (known after apply)
+ name = "iaas-nsg-100" [250/486]
+ network_security_group_name = "iaas-nsg"
+ priority = 100
+ protocol = "Tcp"
+ resource_group_name = "20190611_JS_RG"
+ source_address_prefix = "89.101.76.85/32"
+ source_port_range = "*"
}
# azurerm_public_ip.iaaspubip will be created
+ resource "azurerm_public_ip" "iaaspubip" {
+ allocation_method = "Dynamic"
+ domain_name_label = "js-20190611-iaas-demo"
+ fqdn = (known after apply)
+ id = (known after apply)
+ idle_timeout_in_minutes = 4
+ ip_address = (known after apply)
+ ip_version = "IPv4"
+ location = "uksouth"
+ name = "iaas-pubip"
+ public_ip_address_allocation = (known after apply)
+ resource_group_name = "20190611_JS_RG"
+ sku = "Basic"
+ tags = (known after apply)
}
# azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
+ id = (known after apply)
+ location = "uksouth"
+ name = "20190611_JS_RG"
+ tags = (known after apply)
}
# azurerm_subnet.subnet will be created
+ resource "azurerm_subnet" "subnet" {
+ address_prefix = "10.0.1.0/24"
+ id = (known after apply)
+ ip_configurations = (known after apply)
+ name = "20190611_JS_subnet"
+ resource_group_name = "20190611_JS_RG"
+ virtual_network_name = "20190611_JS_vnet"
}
# azurerm_virtual_machine.main will be created
+ resource "azurerm_virtual_machine" "main" {
+ availability_set_id = (known after apply)
+ delete_data_disks_on_termination = true
+ delete_os_disk_on_termination = true
+ id = (known after apply)
+ license_type = (known after apply)
+ location = "uksouth"
+ name = "iaas-vm"
+ network_interface_ids = (known after apply)
+ resource_group_name = "20190611_JS_RG"
+ tags = (known after apply)
+ vm_size = "Standard_DS1_v2"
+ identity {
+ identity_ids = (known after apply)
+ principal_id = (known after apply)
+ type = (known after apply)
}
+ os_profile {
+ admin_password = (sensitive value)
+ admin_username = "tf_admin"
+ computer_name = "iaas"
+ custom_data = (known after apply)
}
+ os_profile_linux_config {
+ disable_password_authentication = false
}
+ storage_data_disk {
+ caching = (known after apply)
+ create_option = (known after apply)
+ disk_size_gb = (known after apply)
+ lun = (known after apply)
+ managed_disk_id = (known after apply)
+ managed_disk_type = (known after apply)
+ name = (known after apply)
+ vhd_uri = (known after apply)
+ write_accelerator_enabled = (known after apply)
}
+ storage_image_reference {
+ offer = "UbuntuServer"
+ publisher = "Canonical"
+ sku = "18.04-LTS"
+ version = "latest"
}
+ storage_os_disk {
+ caching = "ReadWrite"
+ create_option = "FromImage"
+ disk_size_gb = (known after apply)
+ managed_disk_id = (known after apply)
+ managed_disk_type = "Standard_LRS"
+ name = "iaas-os-disk"
+ os_type = (known after apply)
+ write_accelerator_enabled = false
}
}
# azurerm_virtual_network.vnet will be created [144/486]
+ resource "azurerm_virtual_network" "vnet" {
+ address_space = [
+ "10.0.0.0/16",
]
+ dns_servers = [
+ "8.8.8.8",
+ "8.8.4.4",
]
+ id = (known after apply)
+ location = "uksouth"
+ name = "20190611_JS_vnet"
+ resource_group_name = "20190611_JS_RG"
+ tags = (known after apply)
+ subnet {
+ address_prefix = (known after apply)
+ id = (known after apply)
+ name = (known after apply)
+ security_group = (known after apply)
}
}
Plan: 8 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
This plan was saved to: tfout
To perform exactly these actions, run the following command to apply:
terraform apply "tfout"
view raw terraform_plan hosted with ❤ by GitHub
terraform apply tfout
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 1s [id=/subscriptions/decafbad-1234-abcd-5678-abcdef123456/resourceGroups/MFIOT201906]
azurerm_network_security_group.iaasnsg: Creating...
azurerm_virtual_network.vnet: Creating...
azurerm_public_ip.iaaspubip: Creating...
azurerm_public_ip.iaaspubip: Creation complete after 4s [id=/subscriptions/decafbad-1234-abcd-5678-abcdef123456/resourceGroups/MFIOT201906/providers/Microsoft.Network/publicIPAddresses/iaas-pubip]
azurerm_network_security_group.iaasnsg: Still creating... [11s elapsed]
azurerm_virtual_network.vnet: Still creating... [11s elapsed]
azurerm_virtual_network.vnet: Creation complete after 11s [id=/subscriptions/decafbad-1234-abcd-5678-abcdef123456/resourceGroups/MFIOT201906/providers/Microsoft.Network/virtualNetworks/MFIOT201906_vnet]
azurerm_network_security_group.iaasnsg: Creation complete after 12s [id=/subscriptions/decafbad-1234-abcd-5678-abcdef123456/resourceGroups/MFIOT201906/providers/Microsoft.Network/networkSecurityGroups/iaas-nsg]
azurerm_network_security_rule.iaasnsgr: Creating...
azurerm_subnet.subnet: Creating...
azurerm_network_security_rule.iaasnsgr: Creation complete after 1s [id=/subscriptions/decafbad-1234-abcd-5678-abcdef123456/resourceGroups/MFIOT201906/providers/Microsoft.Network/networkSecurityGroups/iaas-nsg/securityRules/iaas-nsg-100]
azurerm_subnet.subnet: Still creating... [10s elapsed]
azurerm_subnet.subnet: Creation complete after 10s [id=/subscriptions/decafbad-1234-abcd-5678-abcdef123456/resourceGroups/MFIOT201906/providers/Microsoft.Network/virtualNetworks/MFIOT201906_vnet/subnets/MFIOT201906_subnet]
azurerm_network_interface.iaasnic: Creating...
azurerm_network_interface.iaasnic: Creation complete after 0s [id=/subscriptions/decafbad-1234-abcd-5678-abcdef123456/resourceGroups/MFIOT201906/providers/Microsoft.Network/networkInterfaces/iaas-nic]
azurerm_virtual_machine.main: Creating...
azurerm_virtual_machine.main: Still creating... [10s elapsed]
azurerm_virtual_machine.main: Still creating... [20s elapsed]
azurerm_virtual_machine.main: Still creating... [30s elapsed]
azurerm_virtual_machine.main: Provisioning with 'remote-exec'...
azurerm_virtual_machine.main (remote-exec): Connecting to remote host via SSH...
azurerm_virtual_machine.main (remote-exec): Host: myfirstiaasonterraform.uksouth.cloudapp.azure.com
azurerm_virtual_machine.main (remote-exec): User: tf_admin
azurerm_virtual_machine.main (remote-exec): Password: true
azurerm_virtual_machine.main (remote-exec): Private key: false
azurerm_virtual_machine.main (remote-exec): Certificate: false
azurerm_virtual_machine.main (remote-exec): SSH Agent: true
azurerm_virtual_machine.main (remote-exec): Checking Host Key: false
azurerm_virtual_machine.main (remote-exec): Connecting to remote host via SSH...
azurerm_virtual_machine.main (remote-exec): Host: myfirstiaasonterraform.uksouth.cloudapp.azure.com
azurerm_virtual_machine.main (remote-exec): User: tf_admin
azurerm_virtual_machine.main (remote-exec): Password: true
azurerm_virtual_machine.main (remote-exec): Private key: false
azurerm_virtual_machine.main (remote-exec): Certificate: false
azurerm_virtual_machine.main (remote-exec): SSH Agent: true
azurerm_virtual_machine.main (remote-exec): Checking Host Key: false
azurerm_virtual_machine.main (remote-exec): Connecting to remote host via SSH...
azurerm_virtual_machine.main (remote-exec): Host: myfirstiaasonterraform.uksouth.cloudapp.azure.com
azurerm_virtual_machine.main (remote-exec): User: tf_admin
azurerm_virtual_machine.main (remote-exec): Password: true
azurerm_virtual_machine.main (remote-exec): Private key: false
azurerm_virtual_machine.main (remote-exec): Certificate: false
azurerm_virtual_machine.main (remote-exec): SSH Agent: true
azurerm_virtual_machine.main (remote-exec): Checking Host Key: false
azurerm_virtual_machine.main: Still creating... [40s elapsed]
azurerm_virtual_machine.main (remote-exec): Connecting to remote host via SSH...
azurerm_virtual_machine.main (remote-exec): Host: myfirstiaasonterraform.uksouth.cloudapp.azure.com
azurerm_virtual_machine.main (remote-exec): User: tf_admin
azurerm_virtual_machine.main (remote-exec): Password: true
azurerm_virtual_machine.main (remote-exec): Private key: false
azurerm_virtual_machine.main (remote-exec): Certificate: false
azurerm_virtual_machine.main (remote-exec): SSH Agent: true
azurerm_virtual_machine.main (remote-exec): Checking Host Key: false
azurerm_virtual_machine.main (remote-exec): Connected!
azurerm_virtual_machine.main: Provisioning with 'file'...
azurerm_virtual_machine.main: Provisioning with 'remote-exec'...
azurerm_virtual_machine.main (remote-exec): Connecting to remote host via SSH...
azurerm_virtual_machine.main (remote-exec): Host: myfirstiaasonterraform.uksouth.cloudapp.azure.com
azurerm_virtual_machine.main (remote-exec): User: tf_admin
azurerm_virtual_machine.main (remote-exec): Password: true
azurerm_virtual_machine.main (remote-exec): Private key: false
azurerm_virtual_machine.main (remote-exec): Certificate: false
azurerm_virtual_machine.main (remote-exec): SSH Agent: true
azurerm_virtual_machine.main (remote-exec): Checking Host Key: false
azurerm_virtual_machine.main (remote-exec): Connected!
azurerm_virtual_machine.main: Still creating... [50s elapsed]
azurerm_virtual_machine.main (remote-exec): WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
azurerm_virtual_machine.main (remote-exec): WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
azurerm_virtual_machine.main: Still creating... [1m0s elapsed]
azurerm_virtual_machine.main (remote-exec): Extracting templates from packages: 47%
azurerm_virtual_machine.main (remote-exec): Extracting templates from packages: 95%
azurerm_virtual_machine.main (remote-exec): Extracting templates from packages: 100%
azurerm_virtual_machine.main: Still creating... [1m10s elapsed]
azurerm_virtual_machine.main: Still creating... [1m20s elapsed]
azurerm_virtual_machine.main: Still creating... [1m30s elapsed]
azurerm_virtual_machine.main: Still creating... [1m40s elapsed]
azurerm_virtual_machine.main: Still creating... [1m50s elapsed]
azurerm_virtual_machine.main: Still creating... [2m0s elapsed]
azurerm_virtual_machine.main: Still creating... [2m10s elapsed]
azurerm_virtual_machine.main: Still creating... [2m20s elapsed]
azurerm_virtual_machine.main: Still creating... [2m30s elapsed]
azurerm_virtual_machine.main: Still creating... [2m40s elapsed]
azurerm_virtual_machine.main (remote-exec): [WARNING]: No inventory was parsed, only implicit localhost is available
azurerm_virtual_machine.main (remote-exec):
azurerm_virtual_machine.main (remote-exec): [WARNING]: provided hosts list is empty, only localhost is available. Note
azurerm_virtual_machine.main (remote-exec): that the implicit localhost does not match 'all'
azurerm_virtual_machine.main (remote-exec):
azurerm_virtual_machine.main (remote-exec): PLAY [localhost] ***************************************************************
azurerm_virtual_machine.main (remote-exec): TASK [debug] *******************************************************************
azurerm_virtual_machine.main (remote-exec): ok: [localhost] => {
azurerm_virtual_machine.main (remote-exec): "msg": "Hello world!"
azurerm_virtual_machine.main (remote-exec): }
azurerm_virtual_machine.main (remote-exec): PLAY RECAP *********************************************************************
azurerm_virtual_machine.main (remote-exec): localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
azurerm_virtual_machine.main: Creation complete after 2m50s [id=/subscriptions/decafbad-1234-abcd-5678-abcdef123456/resourceGroups/MFIOT201906/providers/Microsoft.Compute/virtualMachines/iaas-vm]
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.
State path: terraform.tfstate
Outputs:
host = myfirstiaasonterraform.uksouth.cloudapp.azure.com
view raw terraform_apply hosted with ❤ by GitHub

Once you’re done with your environment, use terraform destroy to shut it all down… and enjoy :)

The full source is available in the associated Gist. Pull requests and constructive criticism are very welcome!

Featured image is “Seca” by “Olearys” on Flickr and is released under a CC-BY license.

JonTheNiceGuy

He/Him. Husband and father. Linux advocating geek. Co-Host on the AdminAdmin Podcast, occasional conference speaker.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Find out more about Webmentions.)