First of all, this example is made in CentOS 8, and it should not vary from one Linux distribution to another, it even works the same without problems in Windows or macOS.

We check which version of terraform we have with:

terraform -version

which in my case returns the following:

[operatorfeitam@dev08 terraform-projects]$ terraform -version
Terraform v1.4.1
on linux_amd64

Your version of Terraform is out of date! The latest version
is 1.4.2. You can update by downloading from
[operatorfeitam@dev08 terraform-projects]$ 

It is important to know the version to be able to consult the web documentation of that terraform CLI on the internet at the URL:

Terraform CLI Documentation

If you do not have terraform installed, you can install it following the details in:

How to install terraform on CentOS 8

Once we have teraform and know its version, we create a working directory like 'terraform-azure-linux-server-creation':

mkdir terraform-azure-linux-server-creation

We are located inside this folder and create the following files:

  • : file where the provider module to be used for the specific platform (Azure, Google Cloud, etc.) is specified. The module to be used, the version, various data presented by the provider module in its behavior, etc. are specified.
  • : file where we will create the resources (VMs, networks, public IPs, balancers, etc.) that make up the infrastructure.
  • :
    file where it is specified what information we want to display in terraform when we create an object. For example, the private IP or public IP that has been assigned to a VM to be able to connect to it via ssh.
  • : where we define the variables used that are used in the different resources.

It is not necessary to create these files, you can put everything in one file, but with this structure the four main elements of terraform are better structured. There are other files but for a terraform 'Hello world' they are enough.

cd terraform-azure-linux-server-creation

In this example of first steps with terraform we are going to create a virtual linux server belonging to a subnet which is a segment of a general network, with a private IP assigned to the virtual linux server that we will create, and with a public IP that will be assigned dynamically.

For this we need to have an Azure subscription and the azure CLI to be able to login and manage the subscription information of the account.

The following page details how to install Azure CLI (azure-cli) on Ubuntu 22.04, being similar in other Linux distributions with their own installer:

How to install Azure CLI (package azure-cli) on Ubuntu 22.04 on WSL Windows 10

We check the version of azure CLI that we have running in the console:

az -v

which in this example returns us:

[operatorfeitam@dev08 terraform-projects]$ az -v
azure-cli                         2.38.0

core                              2.38.0
telemetry                          1.0.6

msal                            1.18.0b1
azure-mgmt-resource             21.1.0b1

Python location '/usr/bin/python3.6'
Extensions directory '/home/operatorfeitam/.azure/cliextensions'

Python (Linux) 3.6.8 (default, Aug 24 2020, 17:57:11) 
[GCC 8.3.1 20191121 (Red Hat 8.3.1-5)]

Legal docs and information:

Unable to check if your CLI is up-to-date. Check your internet connection.

Please let us know how we are doing:
and let us know if you're interested in trying out our newest features:
[operatorfeitam@dev08 terraform-projects]$ 

So that terraform can access the Azure subscription where we want to create the virtual server we need to be logged in to that subscription. To see how to log in properly with Azure CLI see the following page:

How to sign in with the Azure CLI and manage associated subscriptions

It is very important if we have more than one associated subscription to be sure that we have as 'default' the Azure subscription where we want to mount this infra, there being the option to specify the Tenant ID and subscription ID as in this example.

In addition to having the login made for the Azure subscription where we want to generate the infra, we need to generate a pair of keys with 'ssh-keygen' to be able to authenticate ourselves to the server with the private key. Do not put a passphrase if we do not want it to be requested in the authentication. The execution example to generate a pair of keys called id_rsa_operatorinfra (I use this name because it is the name of the server administrator that we are going to create 'operatorinfra') is the one shown:

[operatorfeitam@dev08 terraform-azure-linux-server-creation]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/operatorfeitam/.ssh/id_rsa): id_rsa_operatorinfra
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in id_rsa_operatorinfra.
Your public key has been saved in
The key fingerprint is:
SHA256:BvCekBTf+Vw/KL6nssCPbPKSgFsAmxxT+GdC76Xq6Nk operatorfeitam@dev08
The key's randomart image is:
+---[RSA 3072]----+
|  o.+.           |
|.+ o = . .       |
|oo= + + o   .    |
|+. o * + o . o   |
| o  = = S + . o  |
|. o  + . . .   . |
| o ...o   .      |
|. +.+..+.  ..    |
|.+.E =+ oooo     |
[operatorfeitam@dev08 terraform-azure-linux-server-creation]$

First we specify in :

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.0.0"

provider "azurerm" { 
  features {} 

  subscription_id = "aad4e527-3ef6-4378-a599-61f803834bce"
  tenant_id       = "7a292158-a1c7-4f67-99f9-435174e561b1"


where we are indicating the provider that we are going to use, which in this case when having to deploy this infrastructure is the 'hashicorp/azurerm' provider, and we are using a somewhat old version '3.0.0'. It is always best to use the most recent version. For 'azurerm' the different existing versions can be checked at:

Providers hashicorp azurerm

In addition, in this 'azurerm' provider we do not indicate any consideration to be taken into account within 'features' because it is not required in this case, and if we indicate the 'subscription_id' and 'tenant_id' to use from Azure, where we will set up the infrastructure.

Then we specify in the file the variables that we are going to use in the definition of the components of the infra:

variable "azurerm_virtual_network__name" {
  type    = string
  default = "my-resourcegroup"

variable "azurerm_subnet__name" {
  type    = string
  default = "my-subnet"

variable "azurerm_public_ip__name" {
  type    = string
  default = "my-publicip"

variable "azurerm_network_interface__name" {
  type    = string
  default = "my-ni"

variable "azurerm_linux_virtual_machine__name" {
  type    = string
  default = "my-lvm"

variable "username" {
  type    = string
  default = "operatorinfra"

We see that in this example they are only string type variables. But they can be of other simple types such as number and bool, and of collection types such as list, set, map, object and tuple.

In the file we specify the creation of the different objects of this infrastructure, which are:

  • A 'resource group' to which all the components of this infra will belong in Azure
  • A 'virtual network' for the network
  • A 'subnet' for the subnet
  • A 'Public IP' for the linux virtual machine to generate
  • A 'network interface' for the linux virtual machine that will be assigned a private IP from the 'subnet', and with a dynamically assigned 'public IP'
  • A 'linux virtual machine' that will contain a hard disk, will have a size of infra "Standard_DS1_v2", an administrator user named "operatorinfra", and will also be an Ubuntu 18.04-LTS server
resource "azurerm_resource_group" "myrg" { 
  name     = "my-resourcegroup" 
  location = "uksouth" 

resource "azurerm_virtual_network" "myvn" { 
  name                = var.azurerm_virtual_network__name
  resource_group_name =
  location            = azurerm_resource_group.myrg.location
  address_space       = [""]

resource "azurerm_subnet" "mysn" { 
  name                 = var.azurerm_subnet__name 
  resource_group_name  = 
  virtual_network_name = 
  address_prefixes     = [""] 

resource "azurerm_public_ip" "mypi" { 
  name                = var.azurerm_public_ip__name
  location            = "uksouth" 
  resource_group_name = 
  allocation_method   = "Dynamic" 
  sku                 = "Basic" 

resource "azurerm_network_interface" "myni"   { 
  name                = var.azurerm_network_interface__name 
  location            = "uksouth" 
  resource_group_name = 

  ip_configuration { 
    name                          = "ipconfig1" 
    subnet_id                     = 
    private_ip_address_allocation = "Static" 
    private_ip_address            = ""
    public_ip_address_id          =

resource "azurerm_linux_virtual_machine" "mylvm" {
  name                  = var.azurerm_linux_virtual_machine__name
  location              = azurerm_resource_group.myrg.location
  resource_group_name   =
  network_interface_ids = []
  size                  = "Standard_DS1_v2"
  admin_username        = var.username

  admin_ssh_key {
    username   = var.username
    public_key = file("")

  os_disk {
    name                 = "hdd01"
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"

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

  tags = {
    environment = "env"


It is important in this file to look at how we told you to put the public key "" in the linux virtual machine, since we will authenticate with your private key to access as user "operatorinfra", which is the server administrator.

And finally in the file where we specify the variable where we want Terraform to inform us of the public IP that has been dynamically assigned to this new linux virtual machine:

output "azurerm_public_ip__ip_address" {
  value = azurerm_public_ip.mypi.ip_address

Once we have all the code ready, we do not locate it in the project folder and execute:

terraform init

Then we execute to validate that everything fits for terraform:

terraform validate

If the previous command gives a problem or error it must be solved.

Once with the past validation we generate the infra executing:

terraform apply

which at a given moment will inform us of the objects to be created and changed requesting an approval (approval is not requested, it is launched with 'terraform apply -auto-approve') as in this example:

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

Changes to Outputs:
  + azurerm_public_ip__ip_address = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: 

Where we see that it tells us that 6 objects will be created, and it tells us that the public IP that will be generated will be provided at the end in the 'azurerm_public_ip__ip_address' variable.

We write 'yes' and press 'enter', and the creation of the six objects begins, informing the console of the evolution:

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

azurerm_resource_group.myrg: Creating...
azurerm_resource_group.myrg: Creation complete after 1s [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup]
azurerm_public_ip.mypi: Creating...
azurerm_virtual_network.myvn: Creating...
azurerm_public_ip.mypi: Creation complete after 4s [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Network/publicIPAddresses/my-publicip]
azurerm_virtual_network.myvn: Creation complete after 6s [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Network/virtualNetworks/my-resourcegroup]
azurerm_subnet.mysn: Creating...
azurerm_subnet.mysn: Creation complete after 5s [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Network/virtualNetworks/my-resourcegroup/subnets/my-subnet]
azurerm_network_interface.myni: Creating...
azurerm_network_interface.myni: Creation complete after 2s [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Network/networkInterfaces/my-ni]
azurerm_linux_virtual_machine.mylvm: Creating...
azurerm_linux_virtual_machine.mylvm: Still creating... [10s elapsed]
azurerm_linux_virtual_machine.mylvm: Still creating... [20s elapsed]
azurerm_linux_virtual_machine.mylvm: Creation complete after 22s [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Compute/virtualMachines/my-lvm]

Apply complete! Resources: 6 added, 0 changed, 0 destroyed.


azurerm_public_ip__ip_address = ""
[operatorfeitam@dev08 terraform-azure-linux-server-creation]$ 

We see that the public IP does not appear. This is correct, to see the public IPs you must execute:

terraform apply -auto-approve -refresh-only

which will show us the public IP at the end:


This is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan
to record the updated values in the Terraform state without changing any remote objects.


Changes to Outputs:
  ~ azurerm_public_ip__ip_address = "" -> ""

You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.


azurerm_public_ip__ip_address = ""
[operatorfeitam@dev08 terraform-azure-linux-server-creation]$ 

Now we can check in the Azure console that everything is created. And we can also connect by ssh with the private key "id_rsa_operatorinfra" executing for example:

ssh -i id_rsa_operatorinfra operatorinfra@

We confirm the key exchange, and access:

[operatorfeitam@dev08 terraform-azure-linux-server-creation]$ ssh -i id_rsa_operatorinfra operatorinfra@
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-1104-azure x86_64)

 * Documentation:
 * Management:
 * Support:

  System information as of Wed Mar 22 18:58:44 UTC 2023

  System load:  0.0               Processes:           108
  Usage of /:   4.5% of 28.89GB   Users logged in:     0
  Memory usage: 5%                IP address for eth0:
  Swap usage:   0%

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See or run: sudo pro status

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

operatorinfra@my-lvm:~$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet  netmask  broadcast
        inet6 fe80::6245:bdff:fe14:13f5  prefixlen 64  scopeid 0x20<link>
        ether 60:45:bd:14:13:f5  txqueuelen 1000  (Ethernet)
        RX packets 4022  bytes 3114324 (3.1 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2773  bytes 768794 (768.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet  netmask
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 572  bytes 46010 (46.0 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 572  bytes 46010 (46.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0


Do not forget that to eliminate all this infra from Azure you must execute:

terraform destroy

We request approval that we confirm with a 'yes' and an 'enter':

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

azurerm_linux_virtual_machine.mylvm: Destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Compute/virtualMachines/my-lvm]
azurerm_linux_virtual_machine.mylvm: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...crosoft.Compute/virtualMachines/my-lvm, 10s elapsed]
azurerm_linux_virtual_machine.mylvm: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...crosoft.Compute/virtualMachines/my-lvm, 20s elapsed]
azurerm_linux_virtual_machine.mylvm: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...crosoft.Compute/virtualMachines/my-lvm, 30s elapsed]
azurerm_linux_virtual_machine.mylvm: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...crosoft.Compute/virtualMachines/my-lvm, 40s elapsed]
azurerm_linux_virtual_machine.mylvm: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...crosoft.Compute/virtualMachines/my-lvm, 50s elapsed]
azurerm_linux_virtual_machine.mylvm: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...crosoft.Compute/virtualMachines/my-lvm, 1m0s elapsed]
azurerm_linux_virtual_machine.mylvm: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...crosoft.Compute/virtualMachines/my-lvm, 1m10s elapsed]
azurerm_linux_virtual_machine.mylvm: Destruction complete after 1m18s
azurerm_network_interface.myni: Destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Network/networkInterfaces/my-ni]
azurerm_network_interface.myni: Destruction complete after 10s
azurerm_subnet.mysn: Destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Network/virtualNetworks/my-resourcegroup/subnets/my-subnet]
azurerm_public_ip.mypi: Destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Network/publicIPAddresses/my-publicip]
azurerm_subnet.mysn: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...rks/my-resourcegroup/subnets/my-subnet, 10s elapsed]
azurerm_public_ip.mypi: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-....Network/publicIPAddresses/my-publicip, 10s elapsed]
azurerm_subnet.mysn: Destruction complete after 10s
azurerm_virtual_network.myvn: Destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup/providers/Microsoft.Network/virtualNetworks/my-resourcegroup]
azurerm_public_ip.mypi: Destruction complete after 11s
azurerm_virtual_network.myvn: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...twork/virtualNetworks/my-resourcegroup, 10s elapsed]
azurerm_virtual_network.myvn: Destruction complete after 11s
azurerm_resource_group.myrg: Destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-61f803834bce/resourceGroups/my-resourcegroup]
azurerm_resource_group.myrg: Still destroying... [id=/subscriptions/aad4e527-3ef6-4378-a599-...834bce/resourceGroups/my-resourcegroup, 10s elapsed]
azurerm_resource_group.myrg: Destruction complete after 17s

Destroy complete! Resources: 6 destroyed.
[operatorfeitam@dev08 terraform-azure-linux-server-creation]$ 

If something is not deleted by this method then we have to delete it from the Azure portal console.

I leave this example in my GitHub repository: