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.
mkdir -p ~/bin
cd ~/bin
sudo apt update && sudo apt install unzip
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
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
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
https://marketplace.visualstudio.com/items?itemName=mauve.terraform
- ctrl+shift+p (Command palete)
- Type:
default shell
and selectTerminal: Select Default Shell
- 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" { | |
} |
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" | |
} |
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" | |
} |
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 | |
} | |
} |
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" | |
} |
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 | |
} | |
} | |
} |
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. |
$ 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" |
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 |
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.