Blocks vs Attributes

6 minute read

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

Updated: