| name | terraform-baseline-builder |
| description | Generate, review, and refactor Terraform modules and configurations following Equinor Terraform Baseline and HashiCorp best practices. USE FOR: create Terraform module, scaffold module, write Terraform code, review Terraform, refactor Terraform, create variables, create outputs, create resources, Terraform best practices, module structure, naming conventions, Azure Terraform module. DO NOT USE FOR: deploying infrastructure, running terraform apply/plan, CI/CD pipeline setup. |
Terraform Baseline Builder
This skill helps generate, review, and refactor Terraform modules and configurations that follow the Equinor Terraform Baseline and HashiCorp official best practices.
Apply this skill whenever the user asks to write, review, scaffold, or refactor Terraform code.
Core Principles
All generated Terraform code MUST adhere to these principles:
- Simplicity: Module contents should be minimal and simple.
- Consistency: Follow consistent naming conventions, structure, and coding standards.
- Reusability: Design modules to be reusable across different projects and environments.
- Transparency: Purpose should be clear and well-documented.
- Predictability: Module behavior should be predictable.
- Security: Follow security best practices (e.g., Microsoft security recommendations for Azure resources).
- Maintainability: Easy to maintain with minimal technical debt.
Module Structure (Standard)
Every module MUST follow the HashiCorp standard module structure:
.
├── README.md
├── main.tf # Primary entrypoint; all resource/data blocks (or nested module calls)
├── variables.tf # All variable blocks, in alphabetical order
├── outputs.tf # All output blocks, in alphabetical order
├── terraform.tf # Single terraform block: required_version + required_providers
├── locals.tf # Local values (if referenced across multiple files)
├── modules/ # Nested submodules (optional)
│ └── <submodule>/
│ ├── README.md
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── examples/ # Usage examples (optional)
│ └── <example>/
│ └── main.tf
└── test/ # Automated tests (optional)
File Rules
main.tf is the primary entrypoint. For simple modules, all resources go here. For complex modules, split by logical group (e.g., network.tf, storage.tf, compute.tf).
variables.tf and outputs.tf MUST exist even if empty.
terraform.tf contains ONLY the terraform block with required_version and required_providers.
- Use a separate
providers.tf only if provider configuration is complex (e.g., aliasing).
locals.tf for local values referenced in multiple files. File-specific locals go at the top of that file.
Naming Conventions
Repository Naming
- Use the convention
terraform-<PROVIDER>-<NAME> (e.g., terraform-azurerm-key-vault).
- For Azure modules, name after the corresponding Azure CLI group or subgroup (e.g.,
storage, key-vault, log-analytics).
- Separate words with
- consistently.
Resource Naming
- Use descriptive nouns for resource names.
- Do NOT include the resource type in the name (the address already includes it).
- Separate words with underscores.
- Wrap resource type and name in double quotes.
- Use
this as the name when a module creates a single instance of its main resource.
# BAD
resource "azurerm_storage_account" "storage_account_main" { ... }
# GOOD
resource "azurerm_storage_account" "this" { ... }
Variable and Output Naming
- Variables:
<resource>_<block>_<argument> (e.g., account_replication_type)
- Outputs:
<resource>_<block>_<attribute> (e.g., account_id)
- Exception: Omit the module name from variable/output names. In module
storage, use account_name not storage_account_name.
Variables
Declaration Rules
- All resource arguments MUST be exposed as variables.
- ALWAYS include
type and description for every variable.
- If the variable is optional, define a reasonable
default.
- For sensitive variables, set
sensitive = true.
- Prefer simple types (
string, number, bool) over complex types where possible.
Parameter Order (within a variable block)
variable "example" {
type = string
description = "Description of the variable."
default = "value" # optional
sensitive = true # optional
nullable = false # optional
validation { # optional
condition = ...
error_message = "..."
}
}
Description Conventions
When valid values are known, append to the description:
- Exact set known:
Value must be "X" or "Y".
- Range known:
Value must be between X and Y.
- Subset known:
Possible values include X, Y, and Z.
- Format known:
Value must be in F format, e.g. X, Y, and Z.
Always add validation blocks for known value constraints:
variable "kind" {
type = string
description = "The kind of Web App to create. Value must be \"Linux\" or \"Windows\"."
default = "Linux"
validation {
condition = contains(["Linux", "Windows"], var.kind)
error_message = "Kind must be \"Linux\" or \"Windows\"."
}
}
Outputs
Declaration Rules
- All resource attributes MUST be exposed as outputs.
- ALWAYS include a
description for every output.
Parameter Order (within an output block)
output "example_id" {
description = "The ID of the example resource."
value = azurerm_example.this.id
sensitive = false # optional
}
Resources
General Rules
- Configure resources based on provider security recommendations (e.g., Microsoft security recommendations for Azure).
- Use resources that do not require more than
Contributor role at the resource group scope.
- Do NOT create resources that are automatically created by the provider (e.g., Azure hidden resources like the
master database for SQL Server).
- A module should only perform control plane operations, not data plane operations.
Single Resource per Module
- A single module call should create a single instance of the main resource.
- A module should NOT create just a single resource unless that resource requires complex configuration.
Submodules
If a resource is a child of another resource:
- The parent resource should be a module.
- The child resource should be a submodule under
modules/.
- Reference as
module//modules/<child>.
Resource Parameter Order
resource "azurerm_example" "this" {
# 1. count or for_each (meta-arguments first)
count = var.enable ? 1 : 0
# 2. Resource-specific non-block parameters
name = var.name
resource_group_name = var.resource_group_name
location = var.location
# 3. Resource-specific block parameters
site_config {
# ...
}
# 4. Lifecycle block
lifecycle {
prevent_destroy = true
}
# 5. depends_on
depends_on = [azurerm_resource_group.this]
}
Conditional Resources (0 or 1)
Use count with a conditional expression based on a static value (variable or local):
variable "kind" {
type = string
description = "The kind of Web App. Value must be \"Linux\" or \"Windows\"."
default = "Linux"
validation {
condition = contains(["Linux", "Windows"], var.kind)
error_message = "Kind must be \"Linux\" or \"Windows\"."
}
}
resource "azurerm_linux_web_app" "this" {
count = var.kind == "Linux" ? 1 : 0
# ...
}
resource "azurerm_windows_web_app" "this" {
count = var.kind == "Windows" ? 1 : 0
# ...
}
Repeatable Resources (0 or more)
Use for_each with a variable of type map(object({})). Default to {} for no resources:
variable "firewall_rules" {
description = "A map of firewall rules to create."
type = map(object({
name = string
start_ip_address = string
end_ip_address = string
}))
default = {}
}
resource "azurerm_mssql_firewall_rule" "this" {
for_each = var.firewall_rules
name = each.value.name
start_ip_address = each.value.start_ip_address
end_ip_address = each.value.end_ip_address
}
Dynamic Blocks
Repeatable Nested Blocks (0 or more)
Use list(object({})) with dynamic blocks. Default to []:
variable "auth_settings_active_directory" {
description = "A list of Active Directory authentication settings."
type = list(object({
client_id = string
client_secret_setting_name = string
}))
default = []
}
resource "azurerm_linux_web_app" "this" {
# ...
auth_settings {
enabled = length(var.auth_settings_active_directory) == 0 ? false : true
dynamic "active_directory" {
for_each = var.auth_settings_active_directory
content {
client_id = active_directory.value["client_id"]
client_secret_setting_name = active_directory.value["client_secret_setting_name"]
}
}
}
}
Non-Repeatable Nested Blocks (0 or 1)
Use object() with a null default. Wrap in [val] or [] for the for_each:
variable "blob_properties" {
description = "The blob properties for this storage account."
type = object({
versioning_enabled = optional(bool, true)
change_feed_enabled = optional(bool, true)
})
default = {}
}
resource "azurerm_storage_account" "this" {
# ...
dynamic "blob_properties" {
for_each = var.blob_properties != null ? [var.blob_properties] : []
content {
versioning_enabled = blob_properties.value["versioning_enabled"]
change_feed_enabled = blob_properties.value["change_feed_enabled"]
}
}
}
Exceptions for non-repeatable blocks:
- Blocks defined as required by the provider (e.g.,
site_config on azurerm_linux_web_app).
- Blocks that are optional but have an argument to enable/disable (e.g.,
auth_settings with enabled).
Meta-Arguments
Lifecycle
- Use
prevent_destroy = true on stateful resources (databases, storage) to prevent accidental data loss.
- Use
ignore_changes sparingly — heavy use leads to configuration drift.
count vs for_each
- Use
count for conditional creation (0 or 1) based on a boolean/string.
- Use
for_each for repeatable resources where each instance needs distinct values.
- Place
count or for_each as the first argument in the resource block.
Code Formatting and Style
- Run
terraform fmt before committing.
- Run
terraform validate to check syntax.
- Use
# for all comments (not // or /* */).
- Indent two spaces per nesting level.
- Align
= signs for consecutive single-line arguments at the same nesting level.
- Separate top-level blocks with one blank line.
- Place meta-arguments (
count, for_each) first, then arguments, then nested blocks, then lifecycle, then depends_on.
- Let code "build on itself" — define data sources before the resources that reference them.
Version Pinning
- Pin provider versions in
required_providers:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
required_version = ">= 1.9"
}
- Pin module sources to a specific ref or version:
module "storage" {
source = "github.com/equinor/terraform-azurerm-storage?ref=v12.1.1"
# ...
}
- Use Dependabot to keep module versions up to date.
.gitignore
Always commit:
- All
.tf files
.terraform.lock.hcl (dependency lock file)
.gitignore
README.md
Never commit:
terraform.tfstate and terraform.tfstate.*
.terraform.tfstate.lock.info
.terraform/ directory
- Saved plan files
.tfvars files containing sensitive information
Testing
- Write automated tests for all variants of the resources using Terraform native tests (
terraform test).
- Place test files in a
tests/ directory with the .tftest.hcl extension.
- Run tests in CI (e.g., as a pre-merge check in pull requests).
Module Composition
- Keep the module tree flat — prefer one level of child modules.
- Use dependency inversion: pass dependencies as input variables rather than having modules create their own.
- Use data-only modules to encapsulate data source lookups when the retrieval method may change.
Security
- Configure resources based on provider security recommendations.
- Use
sensitive = true for variables containing secrets.
- Never hardcode credentials — use environment variables or a secrets manager.
- Use dynamic provider credentials when possible.
- Modules should only perform control plane operations (not data plane) to avoid firewall workarounds that lower security.
Checklist
Before submitting Terraform code, verify: