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:
- Root Module: This is the entry point of your Terraform configuration. It typically includes the main infrastructure layout, providers, and any global configurations.
- Child Modules: These are the reusable components that define specific pieces of infrastructure (e.g., a virtual network, a storage account, etc.).
- 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:
- Initialize Terraform:
terraform init
- Validate the Configuration:
terraform validate
- 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.mdin 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.