Terraform

Terraform Module Input Design Best Practices (Associate-Ready)

2026-04-19
NicheeLab Editorial Team

ハブ記事: Terraform Modules: Complete Guide

Hub article giving you a single-view picture of Terraform modules: design, distribution, and operations

Module input design drives usability, safety, and future extensibility. Vague typing and sloppy defaults are exactly where unplanned drift and production incidents tend to come from.

Following the behavior described in the official Terraform documentation, this article distills the stable concepts the Associate exam likes to test — type constraints, validation, sensitive, tfvars precedence, and so on — into a form you can apply directly in production.

Basics: Use Inputs to Clearly Define the Module Boundary

Input variables are the contract surface between the outside and inside of a module. By defining name, type, description, and default carefully, you keep unintended values from flowing in and give consumers an API they do not have to guess at.

At the Associate level you are expected to know the basic variable block properties (type, default, description, sensitive, nullable), var. references, how values are supplied from tfvars / CLI / environment variables, and how type checking works at plan and apply time.

  • Make variable names semantically unique; do not bake environment or usage into them (split env out into a separate variable)
  • Write description from the consumer's perspective: what to pass and what is rejected
  • If you set a default, limit it to a value that is safe in production (avoid vague defaults)
  • Always specify an explicit type (avoid any as a rule)

A minimal, readable variables.tf example

variable "name" {
  type        = string
  description = "Logical name (prefix) for the resources created by this module"
}

variable "environment" {
  type        = string
  description = "Environment identifier (e.g., dev, stg, prd)"
}

variable "tags" {
  type        = map(string)
  description = "Common tags. Override with an empty map"
  default     = {}
}

# Caller-side example
module "app" {
  source      = "./modules/app"
  name        = "web"
  environment = var.environment
  tags        = merge(var.tags, { component = "frontend" })
}

Type Constraints and validation: Catch Bad Input Early

Tighten types and spell out business constraints with validation. Use nullable to make it explicit whether null is allowed, so you do not end up with bad values slipping in despite having a default.

sensitive is a display suppression, not encryption. The value is still stored in state, so design assuming you also have backend encryption and access control in place.

  • Pick between string/number/bool/list/map/object intentionally, and avoid any whenever possible
  • Use validation for allow-lists, numeric ranges, CIDR formats, and similar checks
  • Set nullable = false where it matters to reject unintended nulls
  • sensitive = true only masks plan output. State protection is a separate concern

Practical examples of validation, nullable, and sensitive

variable "environment" {
  type        = string
  description = "One of dev / stg / prd"
  validation {
    condition     = contains(["dev", "stg", "prd"], var.environment)
    error_message = "environment must be one of dev, stg, or prd."
  }
}

variable "instance_count" {
  type        = number
  description = "Number of instances to launch (1-10)"
  default     = 2
  nullable    = false
  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 10
    error_message = "instance_count must be in the range 1-10."
  }
}

variable "cidr_block" {
  type        = string
  description = "CIDR notation (e.g., 10.0.0.0/16)"
  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "cidr_block must be valid CIDR notation."
  }
}

variable "db_password" {
  type        = string
  description = "Database password"
  sensitive   = true
  nullable    = false
}

Group with Objects: Pack Related Parameters into One Input

For closely correlated values (network settings, scaling settings, etc.), bundling them into a single object type beats letting individual variables proliferate, and it prevents mismatched combinations.

Terraform 1.3+ lets you express optional attributes inside an object directly in the type. That keeps the caller's required fields to a minimum while documenting the default strategy at the type level.

  • Bundle related values into an object and write the semantics into description
  • Give optional attributes safe defaults (Terraform 1.3+)
  • Represent multiple elements with list(object(...)) or map(object(...))
  • any is hard to debug. Pick the smallest type that survives future changes

Object with optional attributes (Terraform 1.3+)

variable "network" {
  description = "VPC and subnet configuration"
  type = object({
    cidr_block      = string
    private_subnets = list(string)
    public_subnets  = optional(list(string), [])
    dns_hostnames   = optional(bool, true)
  })
}

# Consumer side (inside the module)
locals {
  all_subnets = concat(var.network.private_subnets, var.network.public_subnets)
}

# Example of guarding subnet count consistency with validation (where useful)
variable "az_count" {
  type        = number
  description = "Number of AZs to use"
  default     = 2
  validation {
    condition     = length(var.network.private_subnets) >= var.az_count
    error_message = "private_subnets must be at least as long as az_count."
  }
}

How Values Are Supplied and the Precedence: tfvars vs Env Vars vs CLI

When the same variable has multiple sources, Terraform's precedence decides the final value. The exam loves to test this order. In practice, the steady-state approach is to standardize on per-environment tfvars and only override temporarily on the CLI when you have to.

Auto-loaded files (terraform.tfvars, *.auto.tfvars) are a convenient entry point for team standards. Put long-lived values in tfvars; use the CLI or environment variables for short-lived, one-off values.

  • Precedence (high → low): CLI flags (-var/-var-file) > tfvars (including .auto) > environment variable TF_VAR_* > variable block default
  • terraform.tfvars / *.auto.tfvars are auto-loaded; agree on a filename convention
  • Handle secrets through environment variables or external secret integrations (Vault, SSM, etc.) combined with sensitive
  • For production, commit pinned tfvars and minimize CLI overrides
SourcePriority (1 = highest)ScopePractical use
-var1Per commandTemporary overrides and emergency workarounds. Avoid for steady-state operations
-var-file=PATH1Per commandExplicitly pick an environment-specific file. Great for swapping in CI
terraform.tfvars / *.auto.tfvars2Current directoryStandard per-environment configuration; easy to share across the team
Environment variable TF_VAR_name3Shell or CI process environmentInject secrets or CI-provided values; suppresses log traces
variable block default4Module definitionUse only for safe defaults — never put vague defaults here

How inputs flow through resolution (high priority → low priority)

CLI flags (-var / -var-file)
           |
           v  (highest priority — overrides everything)
Auto-loaded tfvars (terraform.tfvars / *.auto.tfvars)
           |
           v
Environment variables TF_VAR_*
           |
           v
variable block default
           |
           v
Bundled into the root module's var
           |
           v
Passed as arguments to module calls
           |
           v
Applied to resource attributes during plan/apply

Concrete examples from production

# Auto-loaded file (team standard)
# terraform.tfvars
environment = "stg"
name        = "web"

# Explicitly specified for production only (CI)
terraform apply -var-file=envs/prd.tfvars

# Inject secrets from environment variables (CI or local)
TF_VAR_db_password="s3cr3t" terraform plan

# Temporary override (for debugging)
terraform apply -var instance_count=3

Cut Down on enable Flags: Make Inputs Composable

Boolean flags like create or enable are convenient, but pile on too many and you get a combinatorial explosion of behaviors. For future extensibility and testability, designs that pass explicit collections (map/list/object) are easier to maintain.

If you really do need a flag, be disciplined about turning things off: use count or for_each to fully skip resource creation, and make sure dependencies do not form cycles.

  • Pass concrete values via map/object rather than letting flags proliferate
  • Express the do-not-create state as zero items via count or for_each
  • Allow enable_* only in upper (integration) modules; keep lower modules on explicit set inputs
  • Decision order: concrete input first, then composition, then flags as a last resort

Flag-based vs collection-based approaches side by side

# Flag-based approach (keep to a minimum)
variable "create_iam_role" {
  type    = bool
  default = true
}

resource "aws_iam_role" "this" {
  count = var.create_iam_role ? 1 : 0
  name  = "example"
  assume_role_policy = data.aws_iam_policy_document.assume.json
}

# Collection-based approach (recommended)
variable "iam_roles" {
  description = "Map of IAM role definitions to create"
  type = map(object({
    name               = string
    assume_role_policy = string
    tags               = optional(map(string), {})
  }))
  default = {}
}

resource "aws_iam_role" "this_map" {
  for_each            = var.iam_roles
  name                = each.value.name
  assume_role_policy  = each.value.assume_role_policy
  tags                = coalesce(each.value.tags, {})
}

Operating Security, Backward Compatibility, and Docs

sensitive is display masking, not state-file encryption. Assume you have remote backend encryption and permission control in place, and propagate sensitive into outputs too.

Variable schema changes are easy to make breaking. Keep existing inputs intact while adding new fields as optional, then mark the old inputs deprecated before removing them in a later step. Document every change in the CHANGELOG and the variable's description.

  • Chain secrets through both variable.sensitive = true and output.sensitive = true
  • Make state protection (remote backend encryption and access restrictions) mandatory
  • Avoid type changes that break backward compatibility; extend by adding optional attributes instead
  • For deprecations, explicitly mark DEPRECATED in description and give a compatibility window

Examples of propagating secrets and marking deprecations

# Sensitive input and output
variable "db_password" {
  type        = string
  description = "Database password"
  sensitive   = true
}

output "db_password" {
  value     = var.db_password
  sensitive = true
}

# Phase out a deprecated input step by step
variable "instance_type_legacy" {
  type        = string
  default     = null
  description = "DEPRECATED: use compute.instance.type instead. Scheduled for removal in a future major release"
}

variable "compute" {
  description = "Compute resource configuration (new schema)"
  type = object({
    instance = object({
      type = string
      size = optional(number, 1)
    })
  })
}

Check Your Understanding

Associate

問題 1

When the same variable is supplied from multiple sources, which precedence (high → low) does Terraform use to pick the final value?

  1. CLI flags (-var / -var-file) > tfvars (terraform.tfvars / *.auto.tfvars) > environment variables TF_VAR_* > variable block default
  2. tfvars (terraform.tfvars / *.auto.tfvars) > CLI flags (-var / -var-file) > variable block default > environment variables TF_VAR_*
  3. Environment variables TF_VAR_* > CLI flags (-var / -var-file) > tfvars > variable block default
  4. Variable block default > environment variables TF_VAR_* > tfvars > CLI flags (-var / -var-file)

正解: A

Terraform gives CLI flags (-var / -var-file) the highest precedence, followed by tfvars (terraform.tfvars / *.auto.tfvars), then environment variables TF_VAR_*, with the variable block default last. This is the stable behavior described in the official documentation.

Frequently Asked Questions

From which Terraform version are optional attributes on object types available?

Optional attributes and their default expressions inside object types are supported from Terraform 1.3 onward. For modules that span versions, either declare a minimum with required_version or fall back to filling in values through locals as before.

If I set sensitive=true on a variable, is the value fully hidden?

No. sensitive only masks the value in plan output and UI displays. The value is still stored in state, so you need backend encryption (for example Terraform Cloud/Enterprise, or S3 + KMS) together with strict access control. Once the value is passed to an external service, you also need to watch downstream logs.

How should I choose between default and null, and how do I set nullable?

If you want the module to run on a safe default when the input is omitted, set a safe value via default. If the value is required and you cannot accept it being missing, set nullable=false and force an explicit input. null represents an intentional absence, but leaving nullable=true makes it easy to miss an accidental null flowing through. For important parameters, prefer nullable=false.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.