Week 9 – Daily Practice Tasks: Terraform on Azure - snir1551/DevOps-Linux GitHub Wiki

Task 1 – Install and Configure Terraform

terraform_setup.sh Script This script helps you easily install or uninstall Terraform on your Ubuntu/Linux (or WSL) environment.

It does the following:

Option 1: Installs the latest available version of Terraform

Option 2: Completely removes Terraform and its configuration

How to Use the Script

  1. Create the script file:
vim terraform_setup.sh

Paste the following content into it:

#!/bin/bash

echo "Terraform Setup Script"
echo "========================="
echo "1) Install Terraform"
echo "2) Uninstall Terraform"
echo ""

read -p "Choose an option [1-2]: " action

if [[ "$action" == "1" ]]; then
    if command -v terraform &> /dev/null; then
        echo "Terraform is already installed!"
        terraform -v
        exit 0
    fi

    echo "Updating package list..."
    sudo apt-get update -y

    echo "Installing required dependencies (gnupg, software-properties-common, curl)..."
    sudo apt-get install -y gnupg software-properties-common curl

    echo "Downloading and adding HashiCorp GPG key..."
    curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

    echo "Adding HashiCorp APT repository to sources list..."
    DISTRO=$(lsb_release -cs)
    echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com ${DISTRO} main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

    echo "Updating package list after adding HashiCorp repo..."
    sudo apt-get update -y

    echo "Installing Terraform..."
    LATEST_VERSION=$(apt-cache madison terraform | awk '{print $3}' | sort -Vr | head -n1)
    sudo apt-get install -y terraform=$LATEST_VERSION


    echo ""
    echo "Terraform installation complete:"
    terraform -v

elif [[ "$action" == "2" ]]; then
    if ! command -v terraform &> /dev/null; then
        echo "Terraform is not installed."
        exit 0
    fi

    echo "Removing Terraform..."
    sudo apt-get remove --purge -y terraform

    echo "Removing HashiCorp repository and GPG key..."
    sudo rm -f /etc/apt/sources.list.d/hashicorp.list
    sudo rm -f /usr/share/keyrings/hashicorp-archive-keyring.gpg

    echo "Updating package list after cleanup..."
    sudo apt-get update -y

    echo "Terraform and related components were successfully removed."

else
    echo "Invalid option. Please choose 1 or 2."
    exit 1
fi

  1. Make the script executable:
chmod +x terraform_setup.sh
  1. Run the script:
./terraform_setup.sh
  • Type 1 to install Terraform
  • Type 2 to uninstall Terraform
  1. Verify Terraform Installation
terraform -v
  1. Install Terraform Extension in VS Code

    1. Open Visual Studio Code
    2. Go to the Extensions panel (Ctrl + Shift + X)
    3. Search for Terraform
    4. Install the official extension from HashiCorp
  2. Connect to Azure (via CLI)

Make sure Azure CLI is installed. If not, install with:

curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

Then:

az login

It will open a browser window for you to sign in.

Task 2 – Write Basic Terraform Configuration

1. create file main.tf:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "mtc-rg" {
  name     = "mtc-resources"
  location = "Israel Central"

  tags = {
    environment = "dev"
  }
}

Explanation Code:

Line Explanation
terraform { ... } This block defines general settings for the Terraform project.
required_providers Specifies which external providers Terraform should use.
azurerm = { ... } Declares the Azure Resource Manager (azurerm) provider.
source = "hashicorp/azurerm" Tells Terraform to download the official azurerm plugin from the Terraform Registry.
version = "~> 3.0" Ensures compatibility with version 3.0 and above, but below 4.0. This means it will use any version like 3.1, 3.50, 3.117.1, etc., but not 4.x.
Line Explanation
provider "azurerm" { Declares that you're using the Azure Resource Manager (azurerm) provider. All resources that start with azurerm_ (like azurerm_resource_group) rely on this configuration.
features {} A required block for the azurerm provider (since version 2.0+). Even if you don't set any features now, this empty block must be present to activate the provider. It's where you could later enable/disable provider-specific features (like key vault recovery, etc.).
Line Explanation
resource "azurerm_resource_group" "mtc-rg" { Declares a new Azure Resource Group using the azurerm provider. "mtc-rg" is a local Terraform identifier used to reference this resource elsewhere in your configuration.
name = "mtc-resources" Sets the actual name of the resource group that will be created in the Azure portal.
location = "Israel Central" Specifies the Azure region where the resource group will be deployed. "Israel Central" refers to the Azure data center in Israel.
tags = { Begins a block to define metadata tags for the resource group. Tags are used for organizing and managing Azure resources.
environment = "dev" Adds a tag with the key environment and value dev, indicating that this resource group is intended for development.

2. Initialize Terraform

terraform init

3. Review the Plan

terraform plan
  • This command initializes the working directory. It downloads the required providers (like azurerm), sets up backend configuration (if defined), and prepares Terraform to run.

4. Apply the Configuration

terraform apply
  • This shows you a preview of what Terraform is going to do – what resources it will create, change, or destroy.
  • It does not apply any changes yet. It's a dry-run for safety.

5.

terraform state list

6. Verify in Azure Portal

  • Go to the Azure Portal, search for the resource group named mtc-resources, and verify:

  • It exists in the "Israel Central" region.

  • It has the tag environment = dev.

image

Task 3 – Define and Deploy a Virtual Machine

1. main.tf Additions:

resource "azurerm_virtual_network" "mtc-vn" {
  name                = "mtc-network"
  resource_group_name = azurerm_resource_group.mtc-rg.name
  location            = azurerm_resource_group.mtc-rg.location
  address_space       = ["10.123.0.0/16"]
  tags = {
    environment = "dev"
  }
}

resource "azurerm_subnet" "mtc-subnet" {
  name                 = "mtc-subnet"
  resource_group_name  = azurerm_resource_group.mtc-rg.name
  virtual_network_name = azurerm_virtual_network.mtc-vn.name
  address_prefixes     = ["10.123.1.0/24"]
}

resource "azurerm_network_security_group" "mtc-nsg" {
  name                = "mtc-network"
  location            = azurerm_resource_group.mtc-rg.location
  resource_group_name = azurerm_resource_group.mtc-rg.name

  tags = {
    environment = "dev"
  }
}

resource "azurerm_network_security_rule" "mtc-dev-rule" {
  name                        = "mtc-dev-rule"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_ranges     = ["22"]
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.mtc-rg.name
  network_security_group_name = azurerm_network_security_group.mtc-nsg.name
}

resource "azurerm_subnet_network_security_group_association" "mtc-subnet-nsg-association" {
  subnet_id                 = azurerm_subnet.mtc-subnet.id
  network_security_group_id = azurerm_network_security_group.mtc-nsg.id
}

resource "azurerm_public_ip" "mtc-ip" {
  name                = "mtc-ip"
  location            = azurerm_resource_group.mtc-rg.location
  resource_group_name = azurerm_resource_group.mtc-rg.name
  allocation_method   = "Static"

  tags = {
    environment = "dev"
  }

}

resource "azurerm_network_interface" "mtc-nic" {
  name                = "mtc-nic"
  location            = azurerm_resource_group.mtc-rg.location
  resource_group_name = azurerm_resource_group.mtc-rg.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.mtc-subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.mtc-ip.id
  }

  tags = {
    environment = "dev"
  }
  
}

explanation code:

Component Terraform Resource Description
VM azurerm_linux_virtual_machine Defines an Ubuntu VM with SSH-based admin access (no password).
NIC azurerm_network_interface Connects the VM to the network and assigns public/private IP
Public IP azurerm_public_ip Allocates a static IP for remote access
NSG azurerm_network_security_group, azurerm_network_security_rule, azurerm_subnet_network_security_group_association Secures the subnet and opens port 22 (SSH)
  • Virtual Machine Definition with SSH Access:
Line Explanation
resource "azurerm_linux_virtual_machine" "mtc-vm" Defines a new Linux virtual machine in Azure. "mtc-vm" is the local name used in Terraform.
name = "mtc-vm" Sets the actual name of the VM in Azure.
resource_group_name, location References the previously created resource group and region.
size = "Standard_B1s" Specifies a small, cost-effective VM size. Ideal for testing or training environments.
admin_username = "azureuser" Defines the username used to access the VM. You'll log in with ssh azureuser@.
network_interface_ids = [...] Connects the VM to the defined network interface.
admin_ssh_key block Specifies the public SSH key to allow secure login without a password.
username = "azureuser" Must match the admin_username value above.
public_key = file("~/.ssh/mtcazurekey.pub") Loads your local public key file (created using ssh-keygen). Ensure the file exists and path is correct.
os_disk { ... } Configures the OS disk with caching and storage type.
source_image_reference Specifies the OS image to use (Ubuntu 22.04 LTS from Canonical).
tags Adds metadata for organization and filtering in Azure.
  • Define Network Interface (NIC):
Line Explanation
resource "azurerm_network_interface" "mtc-nic" Creates a network interface to attach to the VM.
name = "mtc-nic" Name of the NIC.
location, resource_group_name Standard linking to resource group and region.
ip_configuration { ... } Configures the NIC with:
→ name = "internal" Name of the IP config.
→ subnet_id = ... Connects NIC to a specific subnet.
→ private_ip_address_allocation = "Dynamic" Internal IP will be assigned automatically.
→ public_ip_address_id = ... Links this NIC to a public IP.
tags = ... Tags the NIC as dev.
  • Define Public IP Address
Line Explanation
resource "azurerm_public_ip" "mtc-ip" Creates a static public IP.
allocation_method = "Static" IP will not change (important for CI/CD and consistency).
tags = ... Tags the IP as dev.
  • Define Network Security Group (NSG)
Line Explanation
azurerm_network_security_group Creates an NSG (firewall).
azurerm_network_security_rule Allows SSH (port 22) access from anywhere.
azurerm_subnet_network_security_group_association Binds the NSG to the subnet (so it affects NICs inside).

2. To securely connect to your Azure Virtual Machine using SSH, you need to generate an SSH key pair.

Step 1: Generate SSH Key

Run the following command in your terminal:

ssh-keygen -t rsa -b 4096 -f ~/.ssh/mtcazurekey
Option Description
ssh-keygen Command to generate a new SSH key pair
-t rsa Specifies the key type as RSA
-b 4096 Sets the key length to 4096 bits for better security
-f ~/.ssh/mtcazurekey Path and filename for the key pair (you can customize it)
  • You can press Enter when prompted for a passphrase unless you want to secure it with a password.

Step 2: Confirm the Keys Were Created

ls ~/.ssh/mtcazurekey*

Expected output:

/home/youruser/.ssh/mtcazurekey       # Private key (keep this safe and private)
/home/youruser/.ssh/mtcazurekey.pub   # Public key (this will be used in Terraform)

Step 3: Use Public Key in Terraform

In your main.tf, we used the public key like this:

admin_ssh_key {
  username   = "azureuser"
  public_key = file("~/.ssh/mtcazurekey.pub")
}

2. Apply the Configuration

terraform plan
terraform apply -auto-approve

4. View Detailed VM and IP Information

List All Managed Resources:

terraform state list

This will show you all the resources Terraform is currently managing. Look for a line like: azurerm_linux_virtual_machine.mtc-vm

Inspect the Virtual Machine Resource:

terraform state show azurerm_linux_virtual_machine.mtc-vm

This command will output all properties of the VM, including the attached public IP. Look for:

  • public_ip_address = ...

5. Connect to the VM

Once deployed, use the private key to connect to your VM:

ssh -i ~/.ssh/mtcazurekey azureuser@<PUBLIC_IP>

Replace <PUBLIC_IP> with the actual public IP of your virtual machine (you can find it in the Azure Portal or using terraform output).

6. Create file variables.tf

# Resource Group
variable "resource_group" {
  description = "Resource group configuration"
  type = object({
    name     = string
    location = string
  })
  default = {
    name     = "mtc-resources"
    location = "Israel Central"
  }
}

variable "common_tags" {
  description = "Tags applied to all resources"
  type        = map(string)
  default     = {
    environment = "dev"
  }
}


# Virtual Network
variable "virtual_network" {
  description = "Virtual network configuration"
  type = object({
    name          = string
    address_space = list(string)
  })
  default = {
    name          = "mtc-network"
    address_space = ["10.123.0.0/16"]
  }
}


# Subnet
variable "subnet" {
  description = "Subnet configuration"
  type = object({
    name           = string
    address_prefix = list(string)
  })
  default = {
    name           = "mtc-subnet"
    address_prefix = ["10.123.1.0/24"]
  }
}


# Network Security Group
variable "network_security_group" {
  description = "NSG configuration"
  type = object({
    name = string
  })
  default = {
    name = "mtc-nsg"
  }
}

# Public IP
variable "public_ip" {
  description = "Public IP configuration"
  type = object({
    name              = string
    allocation_method = string
  })
  default = {
    name              = "mtc-ip"
    allocation_method = "Static"
  }
}

# Network Interface
variable "network_interface" {
  description = "NIC configuration"
  type = object({
    name = string
    ip_configuration_name = string
    private_ip_allocation = string
  })
  default = {
    name = "mtc-nic"
    ip_configuration_name = "internal"
    private_ip_allocation = "Dynamic"
  }
}

# Virtual Machine
variable "virtual_machine" {
  description = "Virtual machine configuration"
  type = object({
    name         = string
    size         = string
    admin_user   = string
    ssh_key_path = string
  })
  default = {
    name         = "mtc-vm"
    size         = "Standard_B1s"
    admin_user   = "azureuser"
    ssh_key_path = "~/.ssh/mtcazurekey.pub"
    tags         = { environment = "dev" }
  }
}

variables.tf – Input Variable Definitions The variables.tf file is where we define input variables used throughout the Terraform configuration. Instead of hardcoding values (like names, locations, IP ranges, or VM sizes) directly in main.tf, we define them here as variables to make our code:

  • Reusable – same config can be used in different environments (e.g., dev, prod).

  • Clean and maintainable – no need to repeat values across files.

  • Flexible – can override variables using CLI, environment, or .tfvars.

  1. update your main.tf file:
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "mtc-rg" {
  name     = var.resource_group.name
  location = var.resource_group.location

  tags = {
    environment = var.common_tags.environment
  }
}

resource "azurerm_virtual_network" "mtc-vn" {
  name                = var.virtual_network.name
  resource_group_name = azurerm_resource_group.mtc-rg.name
  location            = azurerm_resource_group.mtc-rg.location
  address_space       = var.virtual_network.address_space
  tags = {
    environment = var.common_tags.environment
  }
}

resource "azurerm_subnet" "mtc-subnet" {
  name                 = var.subnet.name
  resource_group_name  = azurerm_resource_group.mtc-rg.name
  virtual_network_name = azurerm_virtual_network.mtc-vn.name
  address_prefixes     = var.subnet.address_prefix
}

resource "azurerm_network_security_group" "mtc-nsg" {
  name                = var.network_security_group.name
  location            = azurerm_resource_group.mtc-rg.location
  resource_group_name = azurerm_resource_group.mtc-rg.name

  tags = {
    environment = var.common_tags.environment
  }
}

resource "azurerm_network_security_rule" "mtc-dev-rule" {
  name                        = var.network_security_group.name
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_ranges     = ["22"]
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.mtc-rg.name
  network_security_group_name = azurerm_network_security_group.mtc-nsg.name
}

resource "azurerm_subnet_network_security_group_association" "mtc-subnet-nsg-association" {
  subnet_id                 = azurerm_subnet.mtc-subnet.id
  network_security_group_id = azurerm_network_security_group.mtc-nsg.id
}

resource "azurerm_public_ip" "mtc-ip" {
  name                = var.public_ip.name
  location            = azurerm_resource_group.mtc-rg.location
  resource_group_name = azurerm_resource_group.mtc-rg.name
  allocation_method   = "Static"

  tags = {
    environment = var.common_tags.environment
  }

}

resource "azurerm_network_interface" "mtc-nic" {
  name                = var.network_interface.name
  location            = azurerm_resource_group.mtc-rg.location
  resource_group_name = azurerm_resource_group.mtc-rg.name

  ip_configuration {
    name                          = var.network_interface.ip_configuration_name
    subnet_id                     = azurerm_subnet.mtc-subnet.id
    private_ip_address_allocation = var.network_interface.private_ip_allocation
    public_ip_address_id          = azurerm_public_ip.mtc-ip.id
  }

  tags = {
    environment = var.common_tags.environment
  }

}

resource "azurerm_linux_virtual_machine" "mtc-vm" {
  name                = var.virtual_machine.name
  resource_group_name = azurerm_resource_group.mtc-rg.name
  location            = azurerm_resource_group.mtc-rg.location
  size                = var.virtual_machine.size
  admin_username      = var.virtual_machine.admin_user

  network_interface_ids = [azurerm_network_interface.mtc-nic.id]

  admin_ssh_key {
    username   = var.virtual_machine.admin_user
    public_key = file(var.virtual_machine.ssh_key_path)
  }


  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    environment = var.common_tags.environment
  }

}
  1. create new file outputs.tf
output "resource_group_name" {
  description = "The name of the resource group"
  value       = azurerm_resource_group.mtc-rg.name
}

output "public_ip_address" {
  description = "Public IP address of the virtual machine"
  value       = azurerm_public_ip.mtc-ip.ip_address
}

output "virtual_machine_id" {
  description = "ID of the deployed virtual machine"
  value       = azurerm_linux_virtual_machine.mtc-vm.id
}

output "virtual_machine_name" {
  description = "Name of the deployed virtual machine"
  value       = azurerm_linux_virtual_machine.mtc-vm.name
}

output "ssh_connection_command" {
  description = "SSH command to connect to the virtual machine"
  value       = "ssh -i ${var.virtual_machine.ssh_key_path} ${var.virtual_machine.admin_user}@${azurerm_public_ip.mtc-ip.ip_address}"
}

The outputs.tf file in Terraform is used to expose useful information from your infrastructure after it has been deployed. This helps you:

Access important values like public IPs, resource IDs, VM names, etc.

Reference outputs in other Terraform modules or automation scripts.

Provide immediate feedback to users (e.g., a developer can copy/paste an SSH command to connect to the VM).

You can run:

terraform output

To see all defined outputs after deployment. This is especially helpful when working in teams or CI/CD pipelines.

Test the deployment and connect to the VM via SSH to verify functionality:

image

Task 4 – Organize Terraform Code with Modules

Folder Structure:

.
├── main.tf                          # Root file that calls modules
├── variables.tf                    # All input variables for root module
├── outputs.tf                      # Outputs from root module (optional)
├── terraform.tfvars                # (Optional) override default variables
├── providers.tf                    # Provider definitions
├── README.md                       # Project documentation
│
├── modules/
│   ├── resource_group/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   │
│   ├── network/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   │
│   └── virtual_machine/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
│
└── .terraform.lock.hcl             # Auto-generated lock file

modules/resource_group/main.tf

resource "azurerm_resource_group" "this" {
  name     = var.resource_group.name
  location = var.resource_group.location
  tags     = var.tags
}

modules/resource_group/variables.tf

variable "resource_group" {
  description = "Resource group configuration"
  type = object({
    name     = string
    location = string
  })
}

variable "tags" {
  description = "Tags for the resource group"
  type        = map(string)
}

modules/resource_group/outputs.tf

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

output "resource_group_location" {
  value = azurerm_resource_group.this.location
}

modules/network/main.tf

resource "azurerm_virtual_network" "this" {
  name                = var.virtual_network.name
  resource_group_name = var.resource_group_name
  location            = var.location
  address_space       = var.virtual_network.address_space
  tags                = var.tags
}

resource "azurerm_subnet" "this" {
  name                 = var.subnet.name
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.this.name
  address_prefixes     = var.subnet.address_prefix
}

resource "azurerm_network_security_group" "this" {
  name                = var.nsg.name
  location            = var.location
  resource_group_name = var.resource_group_name
  tags                = var.tags
}

resource "azurerm_network_security_rule" "this" {
  name                        = var.nsg.rule_name
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_ranges     = ["22"]
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         = var.resource_group_name
  network_security_group_name = azurerm_network_security_group.this.name
}

resource "azurerm_subnet_network_security_group_association" "this" {
  subnet_id                 = azurerm_subnet.this.id
  network_security_group_id = azurerm_network_security_group.this.id
}

resource "azurerm_public_ip" "this" {
  name                = var.public_ip.name
  location            = var.location
  resource_group_name = var.resource_group_name
  allocation_method   = var.public_ip.allocation_method
  tags                = var.tags
}

resource "azurerm_network_interface" "this" {
  name                = var.network_interface.name
  location            = var.location
  resource_group_name = var.resource_group_name

  ip_configuration {
    name                          = var.network_interface.ip_configuration_name
    subnet_id                     = azurerm_subnet.this.id
    private_ip_address_allocation = var.network_interface.private_ip_allocation
    public_ip_address_id          = azurerm_public_ip.this.id
  }

  tags = var.tags
}

modules/network/variables.tf

variable "resource_group_name" {
  type        = string
  description = "Name of the resource group"
}

variable "location" {
  type        = string
  description = "Azure region"
}

variable "tags" {
  type        = map(string)
  description = "Common tags"
}

variable "virtual_network" {
  type = object({
    name          = string
    address_space = list(string)
  })
}

variable "subnet" {
  type = object({
    name           = string
    address_prefix = list(string)
  })
}

variable "nsg" {
  type = object({
    name      = string
    rule_name = string
  })
}

variable "public_ip" {
  description = "Public IP configuration"
  type = object({
    name              = string
    allocation_method = string
  })
}

variable "network_interface" {
  description = "NIC configuration"
  type = object({
    name                  = string
    ip_configuration_name = string
    private_ip_allocation = string
  })
}

modules/network/outputs.tf

output "subnet_id" {
  value = azurerm_subnet.this.id
}

output "nsg_id" {
  value = azurerm_network_security_group.this.id
}

output "virtual_network_name" {
  description = "The name of the virtual network"
  value       = azurerm_virtual_network.this.name
}

output "network_interface_id" {
  description = "The ID of the network interface (NIC)"
  value       = azurerm_network_interface.this.id
}

output "public_ip_id" {
  description = "The ID of the public IP address"
  value       = azurerm_public_ip.this.id
}

output "public_ip_address" {
  description = "Public IP address of the virtual machine"
  value       = azurerm_public_ip.this.ip_address
}

modules/vm/main.tf

resource "azurerm_linux_virtual_machine" "this" {
  name                = var.vm.name
  resource_group_name = var.resource_group_name
  location            = var.location
  size                = var.vm.size
  admin_username      = var.vm.admin_user

  network_interface_ids = [var.network_interface_id]

  admin_ssh_key {
    username   = var.vm.admin_user
    public_key = file(var.vm.ssh_key_path)
  }

  os_disk {
    caching              = var.vm.disk_caching
    storage_account_type = var.vm.disk_storage_type
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = var.tags
}

modules/vm/variables.tf

variable "resource_group_name" {
  type        = string
  description = "Resource group name"
}

variable "location" {
  type        = string
  description = "Azure region"
}

variable "tags" {
  type        = map(string)
  description = "Common tags"
}

variable "subnet_id" {
  description = "ID of the subnet to associate with the NIC"
  type        = string
}

variable "vm" {
  description = "Virtual machine configuration"
  type = object({
    name                = string
    size                = string
    admin_user          = string
    ssh_key_path        = string
    nic_name            = string
    ip_config_name      = string
    private_ip_alloc    = string
    public_ip_name      = string
    public_ip_alloc     = string
    disk_caching        = string
    disk_storage_type   = string
  })
}

variable "network_interface_id" {
  description = "The ID of the network interface to attach to the VM"
  type        = string
}

modules/vm/outputs.tf

output "virtual_machine_id" {
  description = "ID of the virtual machine"
  value       = azurerm_linux_virtual_machine.this.id
}

output "virtual_machine_name" {
  description = "Name of the virtual machine"
  value       = azurerm_linux_virtual_machine.this.name
}

main.tf

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

# resource "azurerm_resource_group" "imported" {
#   name     = "new_resource"
#   location = "israelcentral"
# }


module "resource_group" {
  source         = "./modules/resource_group"
  resource_group = var.resource_group
  tags           = var.common_tags
}

module "network" {
  source              = "./modules/network"
  resource_group_name = module.resource_group.resource_group_name
  location            = module.resource_group.resource_group_location
  tags                = var.common_tags

  virtual_network = {
    name          = var.virtual_network.name
    address_space = var.virtual_network.address_space
  }

  subnet = {
    name           = var.subnet.name
    address_prefix = var.subnet.address_prefix
  }

  nsg = {
    name      = var.network_security_group.name
    rule_name = "ssh-rule"
  }

  public_ip         = var.public_ip
  network_interface = var.network_interface
}


module "vm" {
  source               = "./modules/vm"
  resource_group_name  = module.resource_group.resource_group_name
  location             = module.resource_group.resource_group_location
  subnet_id            = module.network.subnet_id
  tags                 = var.common_tags
  vm                   = var.virtual_machine
  network_interface_id = module.network.network_interface_id
}

variables.tf

# Resource Group
variable "resource_group" {
  description = "Resource group configuration"
  type = object({
    name     = string
    location = string
  })
  default = {
    name     = "mtc-resources"
    location = "Israel Central"
  }
}

variable "common_tags" {
  description = "Tags applied to all resources"
  type        = map(string)
  default = {
    environment = "dev"
  }
}


# Virtual Network
variable "virtual_network" {
  description = "Virtual network configuration"
  type = object({
    name          = string
    address_space = list(string)
  })
  default = {
    name          = "mtc-network"
    address_space = ["10.123.0.0/16"]
  }
}


# Subnet
variable "subnet" {
  description = "Subnet configuration"
  type = object({
    name           = string
    address_prefix = list(string)
  })
  default = {
    name           = "mtc-subnet"
    address_prefix = ["10.123.1.0/24"]
  }
}


# Network Security Group
variable "network_security_group" {
  description = "NSG configuration"
  type = object({
    name = string
  })
  default = {
    name = "mtc-nsg"
  }
}

# Public IP
variable "public_ip" {
  description = "Public IP configuration"
  type = object({
    name              = string
    allocation_method = string
  })
  default = {
    name              = "mtc-ip"
    allocation_method = "Static"
  }
}

# Network Interface
variable "network_interface" {
  description = "NIC configuration"
  type = object({
    name                  = string
    ip_configuration_name = string
    private_ip_allocation = string
  })
  default = {
    name                  = "mtc-nic"
    ip_configuration_name = "internal"
    private_ip_allocation = "Dynamic"
  }
}

variable "virtual_machine" {
  description = "Virtual machine configuration"
  type = object({
    name              = string
    size              = string
    admin_user        = string
    ssh_key_path      = string
    public_ip_name    = string
    public_ip_alloc   = string
    nic_name          = string
    ip_config_name    = string
    private_ip_alloc  = string
    disk_caching      = string
    disk_storage_type = string
  })
  default = {
    name              = "mtc-vm"
    size              = "Standard_B1s"
    admin_user        = "azureuser"
    ssh_key_path      = "~/.ssh/mtcazurekey.pub"
    public_ip_name    = "mtc-ip"
    public_ip_alloc   = "Static"
    nic_name          = "mtc-nic"
    ip_config_name    = "internal"
    private_ip_alloc  = "Dynamic"
    disk_caching      = "ReadWrite"
    disk_storage_type = "Standard_LRS"
  }
}

outputs.tf

output "resource_group_name" {
  description = "The name of the resource group"
  value       = module.resource_group.resource_group_name
}

output "public_ip_address" {
  description = "Public IP address of the virtual machine"
  value       = module.network.public_ip_address
}

output "virtual_machine_id" {
  description = "ID of the deployed virtual machine"
  value       = module.vm.virtual_machine_id
}

output "virtual_machine_name" {
  description = "Name of the deployed virtual machine"
  value       = module.vm.virtual_machine_name
}

output "ssh_connection_command" {
  description = "SSH command to connect to the virtual machine"
  value       = "ssh -i ${var.virtual_machine.ssh_key_path} ${var.virtual_machine.admin_user}@${module.network.public_ip_address}"
}

In Terraform, a module is a container for multiple resources that are used together. Modules let you organize, reuse, and abstract parts of your infrastructure code, making it easier to manage large or complex projects.

Use modules when:

  • You want to reuse infrastructure logic in different places.

  • You're working on complex infrastructure with many components.

  • You're deploying to multiple environments (e.g., dev, staging, prod).

  • You want to organize code into logical units.

  • You don’t have to use modules in simple or one-time projects.

Each module usually contains the following files:

Test the modularized setup:

terraform plan
terraform apply

Result:

image

Task 5 – Remote State with Azure Storage (with Logging & Debugging)

Step 1: Create Azure Resources for Remote State

Run the following commands using the Azure CLI:

create terraform_setup.sh:

#!/bin/bash

RESOURCE_GROUP="tfstate-rg"
STORAGE_ACCOUNT="snirtfstate"
CONTAINER_NAME="tfstate"
LOCATION="Israel Central"

az group create --name $RESOURCE_GROUP --location "$LOCATION"

az storage account create \
  --name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --sku Standard_LRS \
  --encryption-services blob

az storage container create \
  --name $CONTAINER_NAME \
  --account-name $STORAGE_ACCOUNT \
  --public-access off

echo "Remote state backend setup complete."

Note: The storage account name (e.g., snirtfstate) must be globally unique.

Step 2: Create backend.tf

Create a new file called backend.tf and add the following configuration:

terraform {
  backend "azurerm" {
    resource_group_name   = "tfstate-rg"
    storage_account_name  = "snirtfstate"
    container_name        = "tfstate"
    key                   = "terraform.tfstate"
  }
}

Do not use var. inside the backend block. The values must be hardcoded or passed via CLI.

Step 3: Reinitialize with terraform init

terraform init

Terraform will detect the backend configuration and ask to migrate your local state to remote storage. Confirm with yes.

Step 4: Test the Remote State

Make a small change to test the setup — for example, update your tags:

tags = {
  environment = "prod"
}

Then run:

terraform plan
terraform apply

You’ll see the changes applied and the updated .tfstate file stored in the Azure Blob container.

Step 5: Enable Logging & Debugging

To print full debug logs in your terminal:

export TF_LOG=DEBUG
terraform apply

To save the logs to a file:

terraform apply 2>&1 | tee tf_debug.log

image

Why Use Remote State?

  • Ensures consistency across team members working on the same Terraform project.

  • Azure Blob Storage supports locking and secure state storage.

  • TF_LOG helps with troubleshooting configuration issues and Terraform internals.

Task 6 – Advanced Practice: Import and Cleanup

Create a Manual Resource in Azure

az group create --name new_resource --location israelcentral
  • get SUB_ID to fill dynamicaly into the main.tf
SUB_ID=$(az account show --query id -o tsv | tr -d '\r\n')
  • create main.tf file with the 'SUB_ID'
cd ..
mkdir imported-rg
cd imported-rg
cat <<EOF > main.tf
provider "azurerm" {
  features {
    resource_group {
      prevent_deletion_if_contains_resources = false
    }
  }
  subscription_id = "$SUB_ID"
}

resource "azurerm_resource_group" "imported" {
  name     = "new_resource"
  location = "israelcentral"
}
EOF

Initialize and import

terraform init
terraform import azurerm_resource_group.imported "/subscriptions/$SUB_ID/resourceGroups/new_resource"

Result:

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

Destroy all deployed resources using terraform destroy and verify deletion in the Azure Portal.

terraform destroy
  • Verifed and it deleted.

image

  • verify deletion in the Azure Portal:

image

⚠️ **GitHub.com Fallback** ⚠️