Deploying a virtual machine with Terraform and Ansible
In this second part, we finally get down to the nitty-gritty: vm deployment. Make sure you’ve followed the steps in the previous article, and we’re ready to go
1. Azure
The first thing to do is to connect to Azure and deploy our resources:
cspell:enable ><!-- cspell:disable -->```bashaz login --tenant YOUR-TENANT```
Note that there are several ways to connect to Azure, here with a tenant, but also without. Do an
az login -hto choose the one that suits you most.
2. Terraform
2.1. Creating .tf files
Create a folder for our Terraform code:
cspell:enable ><!-- cspell:disable -->```bashmkdir terraform-vm-tutocd terraform-vm-tuto# and if you're using vscodecode .```In this folder, we’ll create 5 files:
main.tf,variables.tf,providers.tf,network.tfandoutputs.tf.Let’s start with
providers.tf. Its role is to declare the providers that will be used to deploy resources:cspell:enable ><!-- cspell:disable -->```hclterraform {required_providers {azurerm = {source = "hashicorp/azurerm"version = "@>2.0"}tls = {source = "hashicorp/tls"version = "@>4.0"}}}provider "azurerm" {features {}}```Then
main.tf. This is where you declare the resources to be deployed, such as theresource groupand thevirtual machine, as well as their configurations:cspell:enable ><!-- cspell:disable -->```hclresource "azurerm_resource_group" "tf_rg" {location = var.resource_group_locationname = var.resource_group_name}resource "azurerm_linux_virtual_machine" "tf_my_terraform_vm" {name = "myVM"location = azurerm_resource_group.tf_rg.locationresource_group_name = azurerm_resource_group.tf_rg.namenetwork_interface_ids = [azurerm_network_interface.tf_my_terraform_nic.id]size = "Standard_DS1_v2"os_disk {name = "myOsDisk"caching = "ReadWrite"storage_account_type = "Premium_LRS"}source_image_reference {publisher = "Canonical"offer = "0001-com-ubuntu-server-jammy"sku = "22_04-lts-gen2"version = "latest"}computer_name = var.computer_nameadmin_username = var.admin_usernamedisable_password_authentication = trueadmin_ssh_key {username = var.admin_usernamepublic_key = tls_private_key.tf_public_ssh.public_key_openssh}}```Next comes
network.tf. Here you declarevnet,subnetand othernetworkingrelated resources:cspell:enable ><!-- cspell:disable -->```bashresource "azurerm_virtual_network" "tf_my_terraform_network" {name = "myVnet"address_space = ["10.0.0.0/16"]location = azurerm_resource_group.tf_rg.locationresource_group_name = azurerm_resource_group.tf_rg.name}resource "azurerm_subnet" "tf_my_terraform_subnet" {name = "mySubnet"resource_group_name = azurerm_resource_group.tf_rg.namevirtual_network_name = azurerm_virtual_network.tf_my_terraform_network.nameaddress_prefixes = ["10.0.1.0/24"]}resource "azurerm_public_ip" "tf_my_terraform_public_ip" {name = "myPublicIP"location = azurerm_resource_group.tf_rg.locationresource_group_name = azurerm_resource_group.tf_rg.nameallocation_method = "Dynamic"}resource "azurerm_network_security_group" "tf_my_terraform_nsg" {name = "myNetworkSecurityGroup"location = azurerm_resource_group.tf_rg.locationresource_group_name = azurerm_resource_group.tf_rg.namesecurity_rule {name = "SSH"priority = 1001direction = "Inbound"access = "Allow"protocol = "Tcp"source_port_range = "*"destination_port_range = "22"source_address_prefix = "*"destination_address_prefix = "*"}}resource "azurerm_network_interface" "tf_my_terraform_nic" {name = "myNIC"location = azurerm_resource_group.tf_rg.locationresource_group_name = azurerm_resource_group.tf_rg.nameip_configuration {name = "my_nic_configuration"subnet_id = azurerm_subnet.tf_my_terraform_subnet.idprivate_ip_address_allocation = "Dynamic"public_ip_address_id = azurerm_public_ip.tf_my_terraform_public_ip.id}}resource "azurerm_network_interface_security_group_association" "example" {network_interface_id = azurerm_network_interface.tf_my_terraform_nic.idnetwork_security_group_id = azurerm_network_security_group.tf_my_terraform_nsg.id}resource "tls_private_key" "tf_public_ssh" {algorithm = "RSA"rsa_bits = 4096}```The
variables.tffile will contain all the variables used to configure the resources. It’s always a good idea to configure them via variables, as it will be easier to modify them afterwards to suit your needs:cspell:enable ><!-- cspell:disable -->```hclvariable "resource_group_name" {type = stringdefault = "rg-terraform-ansible-tuto"description = "nom du ressource groupe"}variable "resource_group_location" {type = stringdefault = "westeurope"description = "localisation du ressource groupe"}variable "computer_name" {type = stringdefault = "myvm"description = "nom de la machine virtual"}variable "admin_username" {type = stringdefault = "azureadmin"description = "nom de l'utilisateur admin"}variable "vnet_address_space" {type = list(string)default = ["10.0.0.0/16"]description = "plage ip du virtual network"}variable "subnet_address_space" {type = list(string)default = ["10.0.1.0/24"]description = "plage ip du sub-network"}```And
outputs.tf:cspell:enable ><!-- cspell:disable -->```hcloutput "public_ip_address" {value = azurerm_linux_virtual_machine.tf_my_terraform_vm.public_ip_address}output "tls_private_key" {value = tls_private_key.tf_public_ssh.private_key_pemsensitive = true}```
2.1.1. Deploying multiple vm
In the code above, we’ve only deployed one virtual machine, so if we want to deploy 2 or 3, we’ll just have to create more
blocks. You can also use themeta-argument:count.To do this, add :
cspell:enable ><!-- cspell:disable -->```hclresource "azurerm_linux_virtual_machine" "tf_my_terraform_vm" {count = 3 # le nombre de vm qui sera déployé est de 3, par exmeplename = var.vm_names[count.index]location = azurerm_resource_group.tf_rg.locationresource_group_name = azurerm_resource_group.tf_rg.namenetwork_interface_ids = [azurerm_network_interface.tf_my_terraform_nic.id]size = "Standard_DS1_v2"os_disk {name = var.vm_os_disk_names[count.index]caching = "ReadWrite"storage_account_type = "Premium_LRS"}source_image_reference {publisher = "Canonical"offer = "0001-com-ubuntu-server-jammy"sku = "22_04-lts-gen2"version = "latest"}computer_name = var.computer_names[count.index]admin_username = var.admin_username[count.index]disable_password_authentication = trueadmin_ssh_key {username = var.admin_username[count.index]public_key = tls_private_key.tf_public_ssh.public_key_openssh}}```We modify some variables in the
variables.tffile:cspell:enable ><!-- cspell:disable -->```hclvariable "computer_name" {type = list(string)default = ["myvm","myvm2","myvm3"]description = "nom de la machine virtual"}variable "admin_username" {type = list(string)default = ["azureadmin","azureadmin2","azureadmin3"]description = "nom de l'utilisateur admin"}```
2.2 Initialization and deployment
Now that the files have been created, we’re going to initialize Terraform. To do this, go to the root of the folder created above:
cspell:disable >cspell:enable >Terminal window terraform initOnce initialization is complete, we can run an optional command to check the syntax of our code:
cspell:disable >cspell:enable >Terminal window terraform validateOnce our code has been verified, we’ll generate the
cspell:disable >planusing the command :cspell:enable >Terminal window terraform plan -out main.tfplanNote that you can name your
planwhatever you like, here we’ve chosenmain.tfplan.This is the moment we’ve been waiting for! With this command, we can finally deploy our vm :
cspell:disable >cspell:enable >Terminal window terraform apply "main.tfplanNote that it’s possible to do a
terraform applywithout using aterraform plan, but it’s always advisable to generate aplan, as it’s very useful for consistent, stable deployment.Thanks to the
cspell:disable >outputswe’ll get our vm’s ip address and private ssh key:
# ip recovery terraform output public_ip_address
# retrieve private ssh key terraform output -raw tls_private_key > @/.ssh/vm_id_rsaBefore connecting to the vm, we must first restrict access to the private key:
cspell:disable >cspell:enable >Terminal window chmod 600 @/.ssh/vm_id_rsaWe can now try to connect to the vm:
cspell:disable >cspell:enable >Terminal window ssh -i @/.ssh/vm_id_rsa azureadmin@<public_ip_address>
- Now we just need to configure it using Ansible, but that’s for the next blog.