Blocks vs Attributes
Blocks vs Attributes
One of the most important schema design decisions is choosing between blocks and attributes. This choice significantly impacts user experience and configuration readability.
What’s the Difference?
Attributes
Attributes use = assignment syntax:
resource "mycloud_environment" "example" {
name = "production"
region = "us-east-1"
tags = ["web", "prod"]
}
Blocks
Blocks use nested structure with {}:
resource "mycloud_environment" "example" {
name = "production"
config {
memory_gb = 4
cpu_cores = 2
}
health_check {
path = "/health"
interval = 30
}
}
Syntax Comparison
Attribute Syntax
# Single value
name = "production"
# List
tags = ["web", "prod"]
# Map
labels = {
env = "production"
team = "platform"
}
# Object (inline)
config = {
memory_gb = 4
cpu_cores = 2
}
Block Syntax
# Single block
config {
memory_gb = 4
cpu_cores = 2
}
# Multiple blocks (list)
health_check {
path = "/health"
}
health_check {
path = "/ready"
}
# Nested blocks
network {
vpc_id = "vpc-123"
subnet {
id = "subnet-456"
zone = "us-east-1a"
}
subnet {
id = "subnet-789"
zone = "us-east-1b"
}
}
Schema Definitions
SingleNestedAttribute (Block-like)
"config": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"memory_gb": schema.NumberAttribute{
Required: true,
},
"cpu_cores": schema.NumberAttribute{
Required: true,
},
},
},
HCL Usage (both work):
# Block syntax (preferred)
config {
memory_gb = 4
cpu_cores = 2
}
# Attribute syntax (also valid)
config = {
memory_gb = 4
cpu_cores = 2
}
ListNestedAttribute (Multiple Blocks)
"health_check": schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"path": schema.StringAttribute{
Required: true,
},
"interval": schema.NumberAttribute{
Optional: true,
},
},
},
},
HCL Usage:
# Block syntax (preferred)
health_check {
path = "/health"
interval = 30
}
health_check {
path = "/ready"
interval = 10
}
SetNestedAttribute
"rule": schema.SetNestedAttribute{
Required: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"port": schema.NumberAttribute{
Required: true,
},
"protocol": schema.StringAttribute{
Required: true,
},
},
},
},
HCL Usage:
rule {
port = 80
protocol = "tcp"
}
rule {
port = 443
protocol = "tcp"
}
When to Use Blocks
✅ Use Blocks When:
1. Complex nested structures
network {
vpc_id = "vpc-123"
subnet {
cidr = "10.0.1.0/24"
zone = "us-east-1a"
}
}
2. Repeatable configurations
ingress {
port = 80
protocol = "tcp"
}
ingress {
port = 443
protocol = "tcp"
}
3. Logical grouping of related fields
scaling {
min_instances = 2
max_instances = 10
target_cpu = 70
}
4. Optional complex configurations
# User can omit entire block
monitoring {
enabled = true
metrics = ["cpu", "memory"]
}
5. When similar to AWS/other provider patterns
# Users familiar with this pattern
lifecycle {
create_before_destroy = true
prevent_destroy = true
}
When to Use Attributes
✅ Use Attributes When:
1. Simple primitive values
name = "production"
region = "us-east-1"
count = 3
2. Simple collections
tags = ["web", "prod"]
security_groups = ["sg-123", "sg-456"]
3. Key-value pairs
env_vars = {
NODE_ENV = "production"
DEBUG = "false"
}
4. Top-level identifiers
# Always attributes
id = "env-123"
name = "production"
arn = "arn:aws:..."
Design Patterns
Pattern 1: Configuration Block
Use Case: Group related configuration options
"runtime_config": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"timeout": schema.NumberAttribute{
Optional: true,
},
"memory_limit": schema.NumberAttribute{
Optional: true,
},
"cpu_limit": schema.NumberAttribute{
Optional: true,
},
},
},
runtime_config {
timeout = 300
memory_limit = 512
cpu_limit = 1.0
}
Pattern 2: Multiple Rules/Policies
Use Case: Repeatable security rules, policies, etc.
"firewall_rule": schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"port": schema.NumberAttribute{
Required: true,
},
"protocol": schema.StringAttribute{
Required: true,
},
"source_cidr": schema.StringAttribute{
Required: true,
},
},
},
},
firewall_rule {
port = 80
protocol = "tcp"
source_cidr = "0.0.0.0/0"
}
firewall_rule {
port = 443
protocol = "tcp"
source_cidr = "0.0.0.0/0"
}
Pattern 3: Optional Advanced Configuration
Use Case: Advanced settings most users don’t need
"advanced": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"connection_pool_size": schema.NumberAttribute{
Optional: true,
},
"keep_alive_timeout": schema.NumberAttribute{
Optional: true,
},
},
},
# Most users omit this
advanced {
connection_pool_size = 100
keep_alive_timeout = 60
}
Pattern 4: Deeply Nested Structures
"deployment": schema.SingleNestedAttribute{
Required: true,
Attributes: map[string]schema.Attribute{
"strategy": schema.StringAttribute{
Required: true,
},
"rolling_update": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"max_surge": schema.NumberAttribute{
Optional: true,
},
"max_unavailable": schema.NumberAttribute{
Optional: true,
},
},
},
},
},
deployment {
strategy = "rolling"
rolling_update {
max_surge = 1
max_unavailable = 0
}
}
Block Types
Single Nested Attribute
One occurrence only
schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{...},
}
# Only one allowed
config {
key = "value"
}
List Nested Attribute
Multiple occurrences, ordered
schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{...},
},
}
# Multiple allowed, order preserved
item {
priority = 1
}
item {
priority = 2
}
Set Nested Attribute
Multiple occurrences, unordered, unique
schema.SetNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{...},
},
}
# Multiple allowed, order doesn't matter
rule {
port = 80
}
rule {
port = 443
}
User Experience Considerations
Readability
Blocks are more readable for complex structures:
# ✅ Good: Block syntax is clear
network {
vpc_id = "vpc-123"
subnet {
cidr = "10.0.1.0/24"
}
security_group {
id = "sg-456"
}
}
# ❌ Less clear: Attribute syntax
network = {
vpc_id = "vpc-123"
subnet = [{
cidr = "10.0.1.0/24"
}]
security_group = [{
id = "sg-456"
}]
}
Discoverability
Blocks appear in autocomplete and validation:
resource "mycloud_app" "example" {
name = "app"
# IDE suggests: config, network, scaling, monitoring
<TAB>
}
Consistency
Follow patterns users know:
# AWS-style (familiar)
lifecycle {
create_before_destroy = true
}
# Kubernetes-style (familiar)
metadata {
labels = {
app = "web"
}
}
Common Mistakes
❌ Mistake 1: Block for Simple Values
// Bad: Block for single value
"name": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"value": schema.StringAttribute{
Required: true,
},
},
}
# Confusing
name {
value = "production"
}
# Should be
name = "production"
❌ Mistake 2: Attribute for Complex Nested Data
// Bad: Map attribute for complex structure
"network": schema.MapAttribute{
ElementType: types.StringType,
}
# Awkward
network = {
vpc_id = "vpc-123"
subnet_id = "subnet-456"
sg_id = "sg-789"
}
# Better as block
network {
vpc_id = "vpc-123"
subnet_id = "subnet-456"
security_group {
id = "sg-789"
}
}
❌ Mistake 3: Too Many Nested Levels
// Bad: 4+ levels deep
"level1": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"level2": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"level3": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"level4": schema.SingleNestedAttribute{
// TOO DEEP!
},
},
},
},
},
},
}
Guideline: Keep nesting to 2-3 levels maximum.
Decision Tree
Is this a single primitive value?
├─ YES → Use attribute
└─ NO
└─ Is this a collection of primitives?
├─ YES → Use attribute (list, map, set)
└─ NO
└─ Is this a complex structure?
├─ Single occurrence → SingleNestedAttribute
├─ Multiple ordered → ListNestedAttribute
└─ Multiple unordered → SetNestedAttribute
Real-World Examples
AWS EC2 Instance Pattern
resource "aws_instance" "example" {
ami = "ami-123" # Attribute
instance_type = "t2.micro" # Attribute
# Block for complex configuration
ebs_block_device {
device_name = "/dev/sda1"
volume_size = 20
}
# Block for network
network_interface {
network_interface_id = "eni-123"
device_index = 0
}
tags = { # Attribute (map)
Name = "example"
}
}
Kubernetes Deployment Pattern
resource "kubernetes_deployment" "example" {
metadata { # Block
name = "app"
namespace = "default"
labels = { # Attribute
app = "web"
}
}
spec { # Block
replicas = 3 # Attribute
selector { # Block
match_labels = { # Attribute
app = "web"
}
}
template { # Block
metadata { # Block
labels = { # Attribute
app = "web"
}
}
}
}
}
Best Practices
1. Prefer Blocks for Structure
Use blocks when there are 3+ related fields:
// ✅ Good
"scaling": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"min": schema.NumberAttribute{...},
"max": schema.NumberAttribute{...},
"target_cpu": schema.NumberAttribute{...},
},
}
2. Keep Primitives as Attributes
// ✅ Good
"name": schema.StringAttribute{...}
"count": schema.NumberAttribute{...}
"enabled": schema.BoolAttribute{...}
3. Collections as Attributes (Usually)
// ✅ Good for simple collections
"tags": schema.ListAttribute{
ElementType: types.StringType,
}
"env_vars": schema.MapAttribute{
ElementType: types.StringType,
}
4. Use Blocks for Repeatability
// ✅ Good for multiple rules
"ingress_rule": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{...},
}
Key Takeaways
- ✅ Attributes use
=, blocks use{} - ✅ Use attributes for simple values and collections
- ✅ Use blocks for complex nested structures
- ✅ Blocks improve readability for multi-field configurations
- ✅ Follow patterns users expect from other providers
- ✅ Keep nesting depth reasonable (2-3 levels max)
- ✅ Consider user experience and discoverability
What’s Next?
Now let’s learn about required, optional, and computed attributes.
Next: Required, Optional, and Computed →
Previous: ← Attribute Types
Last updated: June 30, 2026