(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,

Darrell

Reply

or to participate.