Terraform module to define a consistent naming convention by (namespace, tenant, environment, stage, name, [attributes]).
A modern rewrite of cloudposse/terraform-null-label, updated for Terraform 1.5+ with proper type safety, simplified logic, and removal of deprecated patterns.
- Generates a consistent
idfrom configurable label elements - Produces a
tagsmap for AWS resources with label values - Supports context chaining between modules (parent → child inheritance)
- Configurable case transformation (
lower,upper,title,none) - ID length limiting with sha256-based hash suffix for uniqueness
- Custom
descriptor_formatsfor alternative output strings - Full type safety via
optional()object types (no moretype = any)
| Name | Version |
|---|---|
| terraform | >= 1.5.0 |
module "label" {
source = "git::https://github.com/PlatformStackPulse/tf-label.git?ref=v1.0.0"
namespace = "eg"
environment = "us-west-2"
stage = "prod"
name = "app"
attributes = ["public"]
tags = {
BusinessUnit = "Engineering"
}
}
resource "aws_s3_bucket" "example" {
bucket = module.label.id # => "eg-us-west-2-prod-app-public"
tags = module.label.tags
}Pass a parent label's context to child modules to inherit all settings:
module "parent" {
source = "git::https://github.com/PlatformStackPulse/tf-label.git?ref=v1.0.0"
namespace = "acme"
stage = "prod"
name = "platform"
}
module "child" {
source = "git::https://github.com/PlatformStackPulse/tf-label.git?ref=v1.0.0"
name = "worker"
context = module.parent.context
}
# child.id => "acme-prod-worker"module "label" {
source = "git::https://github.com/PlatformStackPulse/tf-label.git?ref=v1.0.0"
namespace = "eg"
stage = "prod"
name = "app"
label_order = ["name", "namespace", "stage"]
}
# label.id => "app-eg-prod"module "label" {
source = "git::https://github.com/PlatformStackPulse/tf-label.git?ref=v1.0.0"
namespace = "eg"
stage = "prod"
name = "myapp"
delimiter = ""
label_value_case = "title"
}
# label.id => "EgProdMyapp"module "label" {
source = "git::https://github.com/PlatformStackPulse/tf-label.git?ref=v1.0.0"
namespace = "acme"
environment = "production"
name = "my-long-service-name"
id_length_limit = 32
}
# label.id => truncated with sha256 hash suffix for uniquenessmodule "label" {
source = "git::https://github.com/PlatformStackPulse/tf-label.git?ref=v1.0.0"
namespace = "acme"
stage = "prod"
name = "api"
descriptor_formats = {
stack = {
format = "%s/%s/%s"
labels = ["namespace", "environment", "stage"]
}
qualified_name = {
format = "%s::%s"
labels = ["namespace", "name"]
}
}
}
# module.label.descriptors["stack"] => "acme//prod"
# module.label.descriptors["qualified_name"] => "acme::api"| Name | Version |
|---|---|
| terraform | >= 1.5.0 |
No providers.
No modules.
No resources.
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| attributes | ID element. Additional attributes (e.g. workers or cluster) to add to id,in the order they appear in the list. New attributes are appended to the end of the list. The elements of the list are joined by the delimiterand treated as a single ID element. |
list(string) |
[] |
no |
| context | Single object for setting entire context at once. See description of individual variables for details. Leave string and numeric variables as null to use default value.Individual variable settings (non-null) override settings in context object, except for attributes and tags, which are merged. |
object({ |
{} |
no |
| delimiter | Delimiter to be used between ID elements. Defaults to - (hyphen). Set to "" to use no delimiter at all. |
string |
null |
no |
| descriptor_formats | Describe additional descriptors to be output in the descriptors output map.Map of maps. Keys are names of descriptors. Values are maps of the form {<br/> format = string<br/> labels = list(string)<br/>}format is a Terraform format string to be passed to the format() function.labels is a list of labels, in order, to pass to format() function.Label values will be normalized before being passed to format() so they will beidentical to how they appear in id.Default is {} (descriptors output will be empty). |
map(object({ |
{} |
no |
| enabled | Set to false to prevent the module from creating any resources. | bool |
null |
no |
| environment | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'. | string |
null |
no |
| id_length_limit | Limit id to this many characters (minimum 6).Set to 0 for unlimited length.Set to null to keep the existing setting, which defaults to 0.Does not affect id_full. |
number |
null |
no |
| label_key_case | Controls the letter case of the tags keys (label names) for tags generated by this module.Does not affect keys of tags passed in via the tags input.Possible values: lower, title, upper.Default value: title. |
string |
null |
no |
| label_order | The order in which the labels (ID elements) appear in the id.Defaults to ["namespace", "environment", "stage", "name", "attributes"]. You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. |
list(string) |
null |
no |
| label_value_case | Controls the letter case of ID elements (labels) as included in id,set as tag values, and output by this module individually. Does not affect values of tags passed in via the tags input.Possible values: lower, title, upper and none (no transformation).Set this to title and set delimiter to "" to yield Pascal Case IDs.Default value: lower. |
string |
null |
no |
| labels_as_tags | Set of labels (ID elements) to include as tags in the tags output.Default is to include all labels. Tags with empty values will not be included in the tags output.Set to [] to suppress all generated tags.Note: The value of the name tag, if included, will be the id, not the name. |
set(string) |
null |
no |
| name | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. This is the only ID element not also included as a tag.The "name" tag is set to the full id string. There is no tag with the value of the name input. |
string |
null |
no |
| namespace | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique. | string |
null |
no |
| regex_replace_chars | Terraform regular expression (regex) string. Characters matching the regex will be removed from the ID elements. If not set, "/[^a-zA-Z0-9-]/" is used to remove all characters other than hyphens, letters and digits. |
string |
null |
no |
| stage | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'. | string |
null |
no |
| tags | Additional tags (e.g. {'BusinessUnit': 'XYZ'}).Neither the tag keys nor the tag values will be modified by this module. |
map(string) |
{} |
no |
| tenant | ID element. A customer identifier, indicating who this instance of a resource is for. | string |
null |
no |
| Name | Description |
|---|---|
| attributes | List of normalized attributes. |
| context | Merged but otherwise unmodified input to this module, to be used as context input to other modules. Note: this version will have null values as defaults, not the values actually used as defaults. |
| delimiter | Delimiter between ID elements. |
| descriptors | Map of descriptors as configured by descriptor_formats. |
| enabled | True if module is enabled, false otherwise. |
| environment | Normalized environment. |
| id | Disambiguated ID string restricted to id_length_limit characters in total. |
| id_full | ID string not restricted in length. |
| id_length_limit | The id_length_limit actually used to create the ID, with 0 meaning unlimited. |
| label_order | The naming order actually used to create the ID. |
| name | Normalized name. |
| namespace | Normalized namespace. |
| normalized_context | Normalized context of this module. |
| regex_replace_chars | The regex_replace_chars actually used to create the ID. |
| stage | Normalized stage. |
| tags | Normalized Tag map. |
| tenant | Normalized tenant. |
Copy exports/context.tf into downstream modules to automatically expose the context variable with the correct type definition. This ensures consistent context passing throughout your module hierarchy.
make dev-setup # Install pre-commit hooks
make fmt # Format code
make validate # Run terraform validate
make lint # Run TFLint
make test # Run unit tests (19 scenarios)
make security # Trivy IaC scan
make all # Full CI pipeline locally19 unit test scenarios covering:
- Basic ID generation and full label composition
- Tag generation and selective
labels_as_tags - Custom delimiters and label ordering
- Case transformations (upper, pascal, none)
- ID length truncation with hash suffix
- Disabled module (empty outputs)
- Regex character replacement
- Context chaining between modules
- Tag key case control
- Tenant in label order
- Descriptor formats
Run tests:
terraform init -backend=false
terraform test| Area | Old (terraform-null-label) | New (tf-label) |
|---|---|---|
| Terraform version | >= 0.13.0 | >= 1.5.0 |
| Context variable type | type = any (no type safety) |
object() with optional() — full type checking |
| Sentinel values | ["unset"] / ["default"] workarounds |
Native null — Terraform 1.0 fixed the underlying bug |
tags_as_list_of_maps |
Present (for old ASG tag format) | Removed — AWS provider 4.0+ uses standard tags = {} |
additional_tag_map |
Present (for ASG propagation) | Removed — no longer needed |
| Hash algorithm | md5() |
sha256() — stronger collision resistance |
descriptor_formats type |
type = any |
Properly typed map(object({format, labels})) |
label_order validation |
None | Validates entries are valid label names |
labels_as_tags default |
Sentinel ["default"] with magic detection |
null = include all labels (idiomatic) |
| Null handling | Complex lookup() + try() + contains() |
Direct field access on typed object |
| Code complexity | ~170 LOC with workaround comments | ~140 LOC clean logic |
-
tags_as_list_of_mapsoutput removed — If you used this for AWS autoscaling group tags, switch to the standardtags = {}syntax (supported since AWS provider 4.0). -
additional_tag_mapinput removed — Merge any additional tags directly into thetagsinput. -
labels_as_tagsdefault — Usenull(include all) instead of["default"]. -
Hash suffix differs — If you use
id_length_limitand the ID exceeds the limit, the 5-character hash suffix uses sha256 instead of md5. This means truncated IDs will differ from the old module. If you don't useid_length_limit, there is no difference.
Both modules produce identical output for all 19 test scenarios (same inputs, same expected outputs) except for the hash suffix when id_length_limit triggers truncation:
Old (md5): acme-production-re-b96f7
New (sha256): acme-production-re-2bbf9
MIT — see LICENSE