Modular Approach in Terraform

Introduction:

Terraform, an open-source infrastructure as code (IaaC) tool by HashiCorp, allows you to define and provision infrastructure using a high-level configuration language. As cloud environments grow increasingly complex, managing Terraform codebases can become a challenge. This is where the modular approach comes in a method of structuring your Terraform code that promotes reusability, maintainability, and scalability.

In this blog, we’ll explore the concept of Terraform modules, why they are beneficial, and how to use them effectively when deploying Azure resources. We’ll also walk through some examples to illustrate the modular approach in action.

1. What is a Terraform Module?

A Terraform module is a container for multiple resources that are used together. Modules are essentially self-contained packages of Terraform configurations that can be reused across different projects and environments. Each module typically consists of the following files:

  • main.tf: The primary configuration file where resources are defined.
  • variables.tf: Contains the variables that are passed to the module.
  • outputs.tf: Defines the outputs that the module returns.
  • versions.tf: Specifies the required Terraform and provider versions.

Modules can be nested, meaning a module can include other modules, leading to a highly organized and structured approach to managing infrastructure as code.

Benefits of Using Modules

  • Reusability: Write once, use anywhere. Modules can be reused across different projects and environments, reducing redundancy and errors.
  • Maintainability: Changes made to a module are reflected wherever the module is used, making it easier to manage large-scale infrastructure.
  • Scalability: Modules enable you to scale your infrastructure quickly by replicating configurations for similar resources.
  • Organization: Modules help in breaking down complex configurations into manageable pieces, making the codebase easier to understand and navigate.

2. Structuring Terraform Code with Modules

A common way to structure a Terraform project using modules is to separate the project into three layers:

  1. Root Module: This is the entry point of your Terraform configuration. It typically includes the main infrastructure layout, providers, and any global configurations.
  2. Child Modules: These are the reusable components that define specific pieces of infrastructure (e.g., a virtual network, a storage account, etc.).
  3. Submodules: Sometimes used within child modules for even finer-grained reuse of common resources or configurations.

Example Directory Structure

Here’s an example of how you might structure a Terraform project using modules:

terraform-azure-project/
│
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│   ├── vnet/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── versions.tf
│   ├── storage_account/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── versions.tf
│   └── compute/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       └── versions.tf

In this structure:

  • main.tf: Contains the root module with the main configuration that calls the child modules.
  • modules/: Directory that contains all the child modules, each representing a specific piece of infrastructure.

3. Example: Deploying Azure Resources Using Modules

Let’s walk through a practical example where we’ll deploy an Azure Virtual Network, a Storage Account, and a Virtual Machine using the modular approach in Terraform.

Step 1: Define the Child Modules

3.1 Virtual Network Module

First, create the vnet module:

modules/vnet/main.tf:

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

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_prefixes
}

modules/vnet/variables.tf:

variable "vnet_name" {
  type = string
}

variable "address_space" {
  type = list(string)
}

variable "location" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "subnet_name" {
  type = string
}

variable "subnet_prefixes" {
  type = list(string)
}

modules/vnet/outputs.tf:

output "vnet_id" {
  value = azurerm_virtual_network.this.id
}

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

3.2 Storage Account Module

Next, create the storage_account module:

modules/storage_account/main.tf:

resource "azurerm_storage_account" "this" {
  name                     = var.storage_account_name
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

modules/storage_account/variables.tf:

variable "storage_account_name" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "location" {
  type = string
}

modules/storage_account/outputs.tf:

output "storage_account_id" {
  value = azurerm_storage_account.this.id
}

3.3 Compute Module

Finally, create the compute module for deploying the virtual machine:

modules/compute/main.tf:

resource "azurerm_virtual_machine" "this" {
  name                  = var.vm_name
  resource_group_name   = var.resource_group_name
  location              = var.location
  network_interface_ids = [var.nic_id]
  vm_size               = var.vm_size

  storage_os_disk {
    name              = "${var.vm_name}-osdisk"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
  }

  storage_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  os_profile {
    computer_name  = var.vm_name
    admin_username = var.admin_username
    admin_password = var.admin_password
  }

  os_profile_linux_config {
    disable_password_authentication = false
  }
}

modules/compute/variables.tf:

variable "vm_name" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "location" {
  type = string
}

variable "nic_id" {
  type = string
}

variable "vm_size" {
  type = string
}

variable "admin_username" {
  type = string
}

variable "admin_password" {
  type = string
}

modules/compute/outputs.tf:

output "vm_id" {
  value = azurerm_virtual_machine.this.id
}

Step 2: Use the Modules in the Root Module

Now, let’s put everything together in the root module.

main.tf:

provider "azurerm" {
  features = {}
}

resource "azurerm_resource_group" "main" {
  name     = "rg-modular-example"
  location = "East US"
}

module "vnet" {
  source              = "./modules/vnet"
  vnet_name           = "example-vnet"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  subnet_name         = "default"
  subnet_prefixes     = ["10.0.1.0/24"]
}

module "storage_account" {
  source                = "./modules/storage_account"
  storage_account_name  = "examplestoracc"
  resource_group_name   = azurerm_resource_group.main.name
  location              = azurerm_resource_group.main.location
}

module "compute" {
  source               = "./modules/compute"
  vm_name              = "example-vm"
  resource_group_name  = azurerm_resource_group.main.name
  location             = azurerm_resource_group.main.location
  nic_id               = module.vnet.subnet_id
  vm_size              = "Standard_DS1_v2"
  admin_username       = "adminuser"
  admin_password       = "P@ssw0rd123!"
}

Step 3: Initialize and Apply

To deploy the infrastructure:

  1. Initialize Terraform:
   terraform init
  1. Validate the Configuration:
   terraform validate
  1. Apply the Configuration:
   terraform apply

This will deploy a virtual network, storage account, and

virtual machine in Azure using the modular approach.

4. Best Practices for Modular Terraform Code

  • Keep Modules Small and Focused: Each module should have a clear, single purpose.
  • Use Version Control: Store your modules in a version-controlled repository, like GitHub, for easier management and updates.
  • Document Modules: Always include a README.md in your modules to explain how they work and what variables they accept.
  • Leverage Existing Modules: Check Terraform’s official module registry for existing modules that might meet your needs before creating your own.
  • Plan for Reusability: Design your modules with different environments and use cases in mind.

The modular approach in Terraform provides a scalable, maintainable, and organized way to manage infrastructure as code. By breaking down complex configurations into reusable modules, you can streamline the process of deploying Azure resources and ensure consistency across your cloud environments. This method not only reduces duplication and potential errors but also makes your Terraform codebase easier to navigate and maintain.

By following the principles and examples outlined in this guide, you can start building more efficient and effective Terraform configurations for your Azure deployments.