Week 9 – Daily Practice Tasks: Terraform on Azure - snir1551/DevOps-Linux GitHub Wiki
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
- 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
- Make the script executable:
chmod +x terraform_setup.sh
- Run the script:
./terraform_setup.sh
- Type 1 to install Terraform
- Type 2 to uninstall Terraform
- Verify Terraform Installation
terraform -v
-
Install Terraform Extension in VS Code
- Open Visual Studio Code
- Go to the Extensions panel (Ctrl + Shift + X)
- Search for Terraform
- Install the official extension from HashiCorp
-
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.
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"
}
}
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. |
terraform init
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.
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.
terraform state list
-
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.
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"
}
}
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.
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.
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)
In your main.tf, we used the public key like this:
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/mtcazurekey.pub")
}
terraform plan
terraform apply -auto-approve
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 = ...
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).
# 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.
- 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
}
}
- 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.
.
├── 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:
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.
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.
terraform init
Terraform will detect the backend configuration and ask to migrate your local state to remote storage. Confirm with yes.
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.
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
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.
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
terraform init
terraform import azurerm_resource_group.imported "/subscriptions/$SUB_ID/resourceGroups/new_resource"
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.
terraform destroy
- Verifed and it deleted.
- verify deletion in the Azure Portal: