Terraform Module Inputs
Introduction
Input variables are a crucial component of Terraform modules that allow you to customize module behavior without modifying the module's source code. Think of inputs as parameters that you pass to a function in programming languages. They enable you to create reusable, flexible, and maintainable infrastructure components.
In this guide, we'll explore how to define, configure, and use input variables in Terraform modules. You'll learn best practices for creating effective module inputs that make your infrastructure code more maintainable and versatile.
Understanding Module Inputs
Terraform module inputs are defined using the variable
block within the module's code. These variables can then be referenced throughout the module to customize its behavior based on the values provided by the module consumer.
The Anatomy of Module Inputs
Module inputs are defined in .tf
files within your module directory, typically in a file named variables.tf
. Here's the basic syntax:
variable "instance_type" {
description = "The type of EC2 instance to launch"
type = string
default = "t2.micro"
}
Let's break down the components:
- variable "name": Declares a variable with a specific name
- description: Documents the purpose of the variable (best practice)
- type: Defines the data type (string, number, bool, list, map, etc.)
- default: Provides a default value (optional)
- validation: Adds validation rules (optional)
- sensitive: Marks the variable as sensitive to hide it in output (optional)
Defining Input Variables
Let's see how to define various types of input variables for your modules:
Basic Variable Types
# String variable
variable "region" {
description = "AWS region to deploy resources"
type = string
default = "us-west-2"
}
# Number variable
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
}
# Boolean variable
variable "enable_monitoring" {
description = "Whether to enable detailed monitoring"
type = bool
default = false
}
Complex Variable Types
Terraform supports more complex data structures for input variables:
# List variable
variable "availability_zones" {
description = "List of availability zones to use"
type = list(string)
default = ["us-west-2a", "us-west-2b"]
}
# Map variable
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {
Environment = "dev"
ManagedBy = "terraform"
}
}
# Object variable
variable "vpc_config" {
description = "VPC configuration"
type = object({
cidr_block = string
enable_dns = bool
subnet_count = number
})
default = {
cidr_block = "10.0.0.0/16"
enable_dns = true
subnet_count = 3
}
}
Adding Validation Rules
You can add validation rules to ensure inputs meet specific criteria:
variable "environment" {
description = "Deployment environment"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be 'dev', 'staging', or 'prod'."
}
}
Using Input Variables Within Modules
Once defined, you can use these variables throughout your module by referencing them with the var.
prefix:
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
count = var.instance_count
tags = merge(
var.tags,
{
Name = "web-${var.environment}"
}
)
}
Providing Values to Module Inputs
When using a module, you specify values for its input variables. There are several ways to provide these values:
Directly in Module Block
module "web_server" {
source = "./modules/web-server"
region = "us-east-1"
instance_type = "t3.micro"
instance_count = 2
enable_monitoring = true
}
Using Local Variables
locals {
environment = "production"
common_tags = {
Project = "Example"
Environment = local.environment
Owner = "DevOps Team"
}
}
module "web_server" {
source = "./modules/web-server"
environment = local.environment
tags = local.common_tags
}
From Variable Files
You can define values in .tfvars
files:
prod.tfvars:
region = "us-east-1"
instance_type = "m5.large"
instance_count = 5
tags = {
Environment = "production"
CostCenter = "12345"
}
Then apply with:
terraform apply -var-file=prod.tfvars
Practical Example: Creating a Reusable VPC Module
Let's create a practical example of a VPC module with well-defined inputs:
Module Structure
modules/
vpc/
main.tf
variables.tf
outputs.tf
README.md
variables.tf
variable "name_prefix" {
description = "Prefix to use for resource naming"
type = string
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrnetmask(var.vpc_cidr))
error_message = "Must be a valid CIDR block."
}
}
variable "azs" {
description = "Availability zones to use"
type = list(string)
}
variable "private_subnets" {
description = "CIDR blocks for private subnets"
type = list(string)
default = []
}
variable "public_subnets" {
description = "CIDR blocks for public subnets"
type = list(string)
default = []
}
variable "enable_nat_gateway" {
description = "Whether to create NAT Gateway"
type = bool
default = false
}
variable "single_nat_gateway" {
description = "Whether to use a single NAT Gateway for all private subnets"
type = bool
default = true
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
main.tf
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(
var.tags,
{
Name = "${var.name_prefix}-vpc"
}
)
}
resource "aws_subnet" "private" {
count = length(var.private_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnets[count.index]
availability_zone = var.azs[count.index % length(var.azs)]
tags = merge(
var.tags,
{
Name = "${var.name_prefix}-private-${count.index + 1}"
Type = "private"
}
)
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnets[count.index]
availability_zone = var.azs[count.index % length(var.azs)]
map_public_ip_on_launch = true
tags = merge(
var.tags,
{
Name = "${var.name_prefix}-public-${count.index + 1}"
Type = "public"
}
)
}
# Additional resources for internet gateway, NAT gateway, route tables, etc.
# would be defined here based on input variables
Using the Module
module "vpc" {
source = "./modules/vpc"
name_prefix = "example"
vpc_cidr = "10.0.0.0/16"
azs = ["us-west-2a", "us-west-2b", "us-west-2c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
tags = {
Environment = "dev"
Project = "example"
}
}
Best Practices for Module Inputs
Let's explore some best practices to make your modules more usable and maintainable:
1. Document Your Variables
Always include a clear description for each variable:
variable "retention_days" {
description = "Number of days to retain logs. Must be between 1 and 30."
type = number
default = 7
}
2. Provide Sensible Defaults
Include reasonable default values when appropriate to make the module easier to use:
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro" # Sensible default
}
3. Use Type Constraints
Always specify the expected type to avoid unexpected behavior:
variable "allowed_cidr_blocks" {
description = "List of CIDR blocks allowed to access"
type = list(string)
default = ["0.0.0.0/0"]
}
4. Add Validation Rules
Include validation to catch errors early:
variable "port" {
description = "Port number"
type = number
default = 80
validation {
condition = var.port > 0 && var.port <= 65535
error_message = "Port must be between 1 and 65535."
}
}
5. Group Related Variables
Consider using object variables to group related inputs:
variable "autoscaling" {
description = "Autoscaling configuration"
type = object({
min_size = number
max_size = number
desired_capacity = number
})
default = {
min_size = 1
max_size = 3
desired_capacity = 2
}
}
6. Handle Sensitive Data Appropriately
Mark sensitive variables accordingly:
variable "database_password" {
description = "Password for database"
type = string
sensitive = true
}
Module Input Flow Diagram
Here's a visual representation of how module inputs work:
Common Patterns for Module Inputs
Feature Flags
Use boolean variables to enable/disable features:
variable "enable_monitoring" {
description = "Whether to enable detailed monitoring"
type = bool
default = false
}
resource "aws_instance" "example" {
# Other configuration...
monitoring = var.enable_monitoring
}
Resource Configurations
Objects are ideal for configuring resources:
variable "lb_config" {
description = "Load balancer configuration"
type = object({
internal = bool
load_balancer_type = string
security_groups = list(string)
subnets = list(string)
})
}
Count-Based Resource Creation
Use number variables for controlling resource counts:
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
}
resource "aws_instance" "example" {
count = var.instance_count
# Other configuration...
}
Summary
Terraform module inputs are essential for creating reusable infrastructure components. They allow you to parameterize your modules, making them flexible and adaptable to different environments and use cases.
Key points to remember:
- Define variables with appropriate types and clear documentation
- Use validation to ensure input correctness
- Provide sensible defaults when possible
- Use object variables to group related inputs
- Mark sensitive data appropriately
By following these principles and patterns, you can create flexible, maintainable, and user-friendly Terraform modules that can be reused across your organization.
Exercises
-
Create a module for an AWS S3 bucket with variables for bucket name, versioning status, and access logging configuration.
-
Modify the VPC module example to support IPv6 CIDR blocks using an optional boolean flag input.
-
Create a security group module with variable inputs for ingress and egress rules using complex types.
-
Build a module for an EC2 instance with a web server that uses validation rules to ensure instance types are from an approved list.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)