- Random Access Musings
- Posts
- (More) Advanced Terraform
(More) Advanced Terraform
Modularization with SSH Key Integration

modularization!
Hello Muser!
Continuing from our previous Terraform exploration, let's dive into the world of modular Terraform configuration. This detailed guide provides a line-by-line breakdown of how to transform your Terraform setup into a well-organized modular structure.
Modules: Terraform's Functions
In programming, functions are reusable pieces of code that can be called with different parameters to perform a task. Similarly, Terraform modules allow you to encapsulate a set of resources and configurations, which can be reused with different inputs across your infrastructure.
Encapsulation: Like a function, a module encapsulates logic, hiding complex details and exposing only necessary interfaces (variables and outputs).
Reusability: Modules can be reused across different projects or within the same project, much like calling a function with different arguments.
Parameterization: Variables in modules work like function parameters, allowing you to pass different values to customize the module's behavior.

Step-by-Step Guide to Full Modularization
Here's an overview of what we're going to do:
1. Organize Modules in a Central Folder
Create a modules
folder to house all your modular code for better organization.
2. Create Subfolders for Each Module
Inside modules
, create subfolders: vm
, network
, and security-group
, for clear resource segregation.
3. Migrate and Refine Code for Each Module
Transfer the relevant Terraform code to the respective subfolders, detailing each module.
4. Define Inputs and Outputs for Modules
In each module's subfolder, use variables.tf
for inputs. These become the "parameters" that are passed into the "function".
5. Update Main Configuration to Reference New Module Paths
Adjust your main Terraform file to reference the new paths under the modules
folder.

Detailed Module Configurations
Network Module
Setup: In modules/network
, create variables.tf
, main.tf
, and outputs.tf
.
Outputs allow you to access information from within a module.
You can find more documentation on them here
modules/network/variables.tf
:
variable "resource_group_name" {
description = "Name of the resource group"
type = string
}
variable "location" {
description = "Name of the location"
type = string
}
variable "vnet_name" {
description = "Name of the Virtual Network"
type = string
}
variable "address_space" {
description = "Address space for the Virtual Network"
default = "10.0.0.0/16"
}
variable "subnet_cidrs" {
type = list(string)
description = "Address space for the Virtual Network"
default = ["10.0.2.0/24"]
}
modules/network/main.tf
:
resource "azurerm_virtual_network" "vnet" {
name = var.vnet_name
address_space = [var.address_space]
location = var.location
resource_group_name = var.resource_group_name
}
# Subnet
resource "azurerm_subnet" "my_subnet" {
name = "MySubnet"
resource_group_name = var.resource_group_name
virtual_network_name = var.vnet_name
address_prefixes = var.subnet_cidrs
}
# Public IP address
resource "azurerm_public_ip" "my_public_ip" {
name = "MyPublicIP"
location = var.location
resource_group_name = var.resource_group_name
allocation_method = "Dynamic"
}
# Network Interface
resource "azurerm_network_interface" "my_nic" {
name = "MyNIC"
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.my_subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.my_public_ip.id
}
}
modules/network/outputs.tf
:
output "nic_id" {
value = azurerm_network_interface.my_nic.id
}
Security Group Module
Setup: In modules/security-group
, set up main.tf
and variables.tf
.
modules/security-group/variables.tf
:
variable "security_group_name" {
description = "Name of the Network Security Group"
type = string
}
variable "location" {
description = "Location for the Security Group"
type = string
}
variable "network_interface_id" {
description = "ID of the NIC"
type = string
}
variable "resource_group_name" {
description = "Name of the resource group"
type = string
}
modules/security-group/main.tf
:
# Network Security Group to allow SSH
resource "azurerm_network_security_group" "my_nsg" {
name = var.security_group_name
location = var.location
resource_group_name = var.resource_group_name
security_rule {
name = "SSH"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
# Associate NSG to NIC
resource "azurerm_network_interface_security_group_association" "myprivatekey" {
network_interface_id = var.network_interface_id
network_security_group_id = azurerm_network_security_group.my_nsg.id
}
VM Module
Setup: In modules/vm
, prepare main.tf
and variables.tf
.
modules/vm/variables.tf
:
variable "resource_group_name" {
description = "Name of the resource group"
type = string
}
variable "location" {
description = "Name of the location"
type = string
}
variable "network_interface_id" {
description = "ID of the NIC"
type = string
}
variable "vm_name" {
description = "Name of the Virtual Machine"
type = string
}
variable "vm_size"
description = "Size of the Virtual Machine
type = string
default = "Standard_DS1_v2"
}
variable "admin_username" {
description = "Admin username for the VM"
type = string
}
variable "admin_pw" {
description = "Admin pw for the VM"
type = string
}
modules/vm/main.tf
:
# Create a tls key
resource "tls_private_key" "vm_ssh_key" {
algorithm = "RSA"
rsa_bits = 4096
}
# Save the private key locally
resource "local_file" "private_key" {
content = tls_private_key.vm_ssh_key.private_key_openssh
filename = "${path.root}/private_key.openssh"
}
# Save the public key locally
resource "local_file" "public_key" {
content = tls_private_key.vm_ssh_key.public_key_openssh
filename = "${path.root}/public_key.pub"
}
resource "azurerm_virtual_machine" "vm" {
name = var.vm_name
location = var.location
resource_group_name = var.resource_group_name
network_interface_ids = [var.network_interface_id]
vm_size = var.vm_size
storage_os_disk {
name = "myosdisk"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
storage_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "16.04-LTS"
version = "latest"
}
os_profile {
computer_name = var.vm_name
admin_username = var.admin_username
admin_password = var.admin_pw
}
os_profile_linux_config {
disable_password_authentication = true
ssh_keys {
path = "/home/${var.admin_username}/.ssh/authorized_keys"
key_data = tls_private_key.vm_ssh_key.public_key_openssh
}
}
}

Integrating Modules into Main Configuration
Reference the modules in your main Terraform script main.tf
in the root directory of your project.
provider "azurerm" {
features {}
}
provider "tls" {}
# Create a Resource Group to manage resources
resource "azurerm_resource_group" "rg" {
name = "MyRG"
location = "East US"
}
module "network" {
source = "./modules/network"
vnet_name = "MyVNet"
# The address space has a default value in the module, but as an illustration we set it explicitly here
address_space = "10.0.0.0/16"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
module "security_group" {
source = "./modules/security-group"
security_group_name = "MySecurityGroup"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
# We reference the nic_id using the output value we created in modules/network/outputs.tf
network_interface_id = module.network.nic_id
}
module "virtual_machine" {
source = "./modules/vm"
vm_name = "MyVM"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_id = module.network.nic_id
admin_username = "adminuser"
admin_pw = "P@55w0rd"
}
While this is arguably more code, it allows you reuse a lot of it without too much work. Want to create another virtual machine? Create another module block and name it "virtual_machine_2"
. You could even modify the virtual machine module to create multiple vms with a single module using the for_each
directive (see the for_each documentation).
When you run terraform plan
with this code, you should get an identical output to what we made with the previous code!
You're well on your way to writing sustainable, scalable infrastructure-as-code! This is a lot of information so feel free to comment or DM me to ask questions!
My LinkedIn Profile: Darrell Tang
Enjoyed this post? Subscribe now and share with your colleagues who might find this useful!
Keep learning and keep growing,
Reply