We can create VMs with Terraform in Azure

How to create VMs in Azure with Terraform

Uğur Akgül
TurkNet Technology
Published in
6 min readJan 23, 2023

--

Hi, in this post we are going to create a VM with Terraform in Microsoft Azure platform.

Creating a VM in Azure is quite simple, thanks to Azure Portal. But if you want to create your VMs in some code blocks, there is plenty of options. One of these options is Terraform. Let’s see what Terraform gives us.

What is Terraform

Terraform is an open-source infrastructure as code software tool that enables you to safely and predictably create, change, and improve infrastructure as described on their website.

If you want to code your infrastructure, Terraform is one of the options, and it is a great one. In this post, we are going to code our infrastructure with Terraform. Let’s begin.

Creating a Service Principal on Azure

If you want to automate some of the workloads on Azure, then you will need Service Principal (SP) accounts. We will use an SP for our automation.

Because only way for Terraform to work on Azure is to connecting it, we will connect Terraform with Azure via a Service Principal. Let’s create our SP.

You will need your subscription ID for this SP. You can get your subscription ID after logging into Azure-CLI or Azure portal and using CLI with below command:

az account show --subscription <subscription_name> --query id

After getting your subscription ID, we can create our SP with below command:

az ad sp create-for-rbac - name <service_principal_name> - role Contributor - scopes /subscriptions/<subscription_id>

Now, after creating our SP, the information about our SP will be on the STDOUT like below:

{
"appId": "someappid",
"displayName": "sp-diplay-name",
"password": "superstrongpassword",
"tenant": "tenantid"
}

Take note of this information. We will use them in configuring Terraform.

After our SP is ready, now we can get to the Terraform part.

Configuring Terraform

First we need to configure Azure connection information for Terraform. This can be done in multiple ways. I prefer to create environment variables and then pass these variables to Terraform in runtime. The other way is to write connection information in the providers.tf file. (Not recommending it)

Creating .connection.env file

Lets create a file named .connection.env (The dot in the beginning means that this is a hidden file) with the configuration below:

export ARM_CLIENT_ID="xxx" <appID>
export ARM_CLIENT_SECRET="xxx" <Password>
export ARM_SUBSCRIPTION_ID="xxx" <SubscriptionID>
export ARM_TENANT_ID="xxx" <TenantID>

After creating the file, we can source the file and create our environment variables.

source .connection.env

Now, our Terraform configuration will be connected to the Azure.

File structure

Our file structure will be like below:

├── .connection.env
├── main.tf
├── outputs.tf
├── providers.tf
└── variables.tf

Let’s create this file structure one by one.

Creating providers.tf file

Terraform will know which provider to use from our providers.tf file. Let’s create a file named providers.tf with below configuration

terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=2.84.0"
}
}
required_version = ">= 1.1.3"
}

provider "azurerm" {
features {}
}

Creating variables.tf file

We will provide any variables to Terraform with variables.tf file. In this case, we are providing only our resource group location.

variable "resource_group_location" {
default = "West Europe"
description = "Location of the resource group."
}

Creating outputs.tf file

We will get any output generated from Terraform with outputs.tf file. In this case we are getting our VM’s resource group name, public IP and private key of the SSH-key.

output "resource_group_name" {
value = azurerm_resource_group.rg.name
}

output "public_ip_address" {
value = azurerm_linux_virtual_machine.my_terraform_vm.public_ip_address
}

output "tls_private_key" {
value = tls_private_key.secureadmin_ssh.private_key_pem
sensitive = true

Creating main.tf file

Terraform will run based on main.tf file. We will add our VM configuration as well as Azure resource configurations to this file.

resource "azurerm_resource_group" "rg" {
location = var.resource_group_location
name = "my-first-terraform-RG"
}

# Create virtual network
resource "azurerm_virtual_network" "my_terraform_network" {
name = "my-first-terraform-network"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}

# Create subnet
resource "azurerm_subnet" "my_terraform_subnet" {
name = "my-first-terraform-subnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.my_terraform_network.name
address_prefixes = ["10.0.1.0/24"]
}

# Create public IPs
resource "azurerm_public_ip" "my_terraform_public_ip" {
name = "my-first-terraform-PublicIP"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Dynamic"
}

# Create Network Security Group and rule
resource "azurerm_network_security_group" "my_terraform_nsg" {
name = "my-first-terraform-NetworkSecurityGroup"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
# Note that this rule will allow all external connections from internet to SSH port

security_rule {
name = "SSH"
priority = 200
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}

# Create network interface
resource "azurerm_network_interface" "my_terraform_nic" {
name = "my-first-terraform-myNIC"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name

ip_configuration {
name = "my-first-terraform-nic-configuration"
subnet_id = azurerm_subnet.my_terraform_subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.my_terraform_public_ip.id
}
}

# Connect the security group to the network interface
resource "azurerm_network_interface_security_group_association" "my-nsg-assoc" {
network_interface_id = azurerm_network_interface.my_terraform_nic.id
network_security_group_id = azurerm_network_security_group.my_terraform_nsg.id
}

# Create (and display) an SSH key
resource "tls_private_key" "secureadmin_ssh" {
algorithm = "RSA"
rsa_bits = 4096
}

# Create virtual machine
resource "azurerm_linux_virtual_machine" "my_terraform_vm" {
name = "my-first-terraform-VM"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_ids = [azurerm_network_interface.my_terraform_nic.id]
size = "Standard_DS1_v2"

os_disk {
name = "my-first-terraform-OsDisk"
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}

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

computer_name = "my-first-terraform-vm"
admin_username = "secureadmin"
disable_password_authentication = true

admin_ssh_key {
username = "secureadmin"
public_key = tls_private_key.secureadmin_ssh.public_key_openssh
}
}

As you can see, in the source_image_reference field, we are referencing to an Ubuntu server. You can change this image as you wish.

NOTE: The security rule created above will expose the VM to internet via port 22. Thread carefully.

After getting these files together. We can now finally use the Terraform commands.

Action!

Now while in the folder where “main.tf” resides, we can use the command to initialize Terraform:

terraform init

After initialization:

terraform plan

Terraform will plan the creation and output this plan to STDOUT. If you want to keep this plan in a file, you can use the command:

terraform plan -out myazureplan

with this command, Terraform will create a file named myazureplan and keep this plan inside of it. If you want to apply this config later, you can use the plan file.

After planning, we can apply this plan with:

terraform apply

and this command will apply the last planned terraform code. If you want to apply the config that you planned and outputted to a file, use:

terraform apply <filename>

After these commands, terraform will create your resource in a short time. You can check the resources on Azure portal.

NOTE: You can use -auto-approve argument to skip interacting when Terraform asks your approval. But this argument has it’s downsides, if your Terraform configuration changed without your knowing, applying it without approval will cause unpredictible changes on your infrastructure.

After the creation is finished, Terraform will output some of the variables that we mentioned in the outputs.tf file. These variables are:

  • Resource Group Name
  • Public IP of the VM
  • Private key of the SSH key

To connect our newly created VM, we will need the SSH private key that Terraform created for us. Terraform created the SSH key and put the public key into the VM and we will get the private key and use it in the SSH connection.

To get the private key:

terraform output -raw tls_private_key > secureadmin_id_rsa

and then we can connect to the VM using SSH command:

ssh -i secureadmin_id_rsa secureadmin@<public_ip_of_VM>

Removing Created Resources

After your experiment is done, you can remove your resources created by Terraform with a Terraform command.

You can plan your destroy operation and then use this plan to destroy, or just simply use destroy command.

To plan destroy operation and see what will be destroyed:

terraform plan -destroy

To destroy resources:

terraform destroy

This command is the equivalent of:

terraform apply -destroy

Conclusion

In this post, we looked at how to create a VM with Terraform. Terraform is a strong infrastructure automation tool, I would strongly advise to add this tool to your technology stack.

If you have any questions feel free to reach out to me.

Thank you all for reading, have a good day !

--

--