LAB22_AWS_Terraform count: Creating Multiple Resources from One Block

Terraform count: Creating Multiple Resources from One Block

☁ AWS ⭐ Beginner count count.index Splat Expressions Conditional Resources

Scenario

Your company runs three environments: dev, staging, and prod. Each environment needs an identically-configured S3 bucket for application logs. Rather than copy-pasting the resource block three times, you want to use Terraform's count meta-argument to create all three from a single resource block.

You will also use count to conditionally create an optional KMS key only when a variable is set to true.

Your Objectives
  • Use count = length(var.environments) and count.index to create one S3 bucket per environment name in a list variable.
  • Use a splat expression (aws_s3_bucket.logs[*].id) to collect all bucket names into an output list.
  • Use count = var.enable_kms ? 1 : 0 to conditionally create a KMS key.
  • Access the conditional resource safely using one(aws_kms_key.optional[*].arn).
  • Explain in comments why for_each is usually preferred over count for named resources.

Additional Context

The count meta-argument is great for homogeneous sets of resources (identical-except-for-index) and for conditional resource creation (count = 0 or 1). However, it has a key limitation: resources are tracked by their index in Terraform state. If you remove an item from the middle of the list, Terraform will want to destroy and recreate resources at all subsequent indices.

For this reason, for_each is preferred when the resources have distinct identities (names, IDs) — because for_each tracks by key, not index, so removing one item doesn't affect others.

versions.tf

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

variables.tf

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "environments" {
  description = "List of environment names. One S3 bucket is created per entry."
  type        = list(string)
  default     = ["dev", "staging", "prod"]
}

variable "bucket_prefix" {
  description = "Prefix for all log bucket names."
  type        = string
  default     = "app-logs"
}

# Conditional KMS key — demonstrates count = 0/1 pattern.
variable "enable_kms" {
  description = "Set to true to create a KMS key for encrypting log buckets."
  type        = bool
  default     = false
}

main.tf

# main.tf

provider "aws" {
  region = var.aws_region
}

data "aws_caller_identity" "current" {}

# count = length(list): creates one resource per item in var.environments.
# count.index: the current iteration index (0, 1, 2, ...).
# var.environments[count.index]: the environment name for this iteration.
resource "aws_s3_bucket" "logs" {
  count  = length(var.environments)
  bucket = "${var.bucket_prefix}-${var.environments[count.index]}-${data.aws_caller_identity.current.account_id}"

  tags = {
    Name        = "${var.bucket_prefix}-${var.environments[count.index]}"
    Environment = var.environments[count.index]
    ManagedBy   = "terraform"
  }
}

# Block all public access for each bucket.
resource "aws_s3_bucket_public_access_block" "logs" {
  count  = length(var.environments)
  bucket = aws_s3_bucket.logs[count.index].id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Conditional resource: count = var.enable_kms ? 1 : 0
# When enable_kms = false, count = 0 and no KMS key is created.
# When enable_kms = true,  count = 1 and one KMS key is created.
resource "aws_kms_key" "optional" {
  count = var.enable_kms ? 1 : 0

  description         = "Optional KMS key for log bucket encryption"
  enable_key_rotation = true

  tags = { ManagedBy = "terraform", Purpose = "log-encryption" }
}

# NOTE on count vs for_each:
# This config stores resources as aws_s3_bucket.logs[0], .logs[1], .logs[2].
# If you remove "staging" from var.environments, Terraform will try to:
#   - destroy aws_s3_bucket.logs[1] (staging)
#   - destroy aws_s3_bucket.logs[2] (prod)
#   - create  aws_s3_bucket.logs[1] (prod) — with a new state key!
# Use for_each to avoid this index-shift problem.

outputs.tf

# Splat expression: collect an attribute from every instance of a count resource.
output "bucket_ids" {
  description = "All log bucket names, one per environment."
  value       = aws_s3_bucket.logs[*].id
}

output "bucket_arns" {
  value = aws_s3_bucket.logs[*].arn
}

# one() returns the single element of a 0-or-1 length list, or null if empty.
# This is the safe way to access a conditional (count=0/1) resource.
output "kms_key_arn" {
  description = "KMS key ARN if enable_kms=true, otherwise null."
  value       = one(aws_kms_key.optional[*].arn)
}

Workflow Commands

terraform init

# Create 3 buckets, no KMS key
terraform apply

# View the indexed state addresses
terraform state list | grep aws_s3_bucket

# Add KMS key by changing the variable
terraform apply -var="enable_kms=true"

# See all bucket names
terraform output -json bucket_ids

terraform destroy -var="enable_kms=true"
✓ Expected Output After Apply
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.

Outputs:

bucket_arns = [
  "arn:aws:s3:::app-logs-dev-123456789012",
  "arn:aws:s3:::app-logs-staging-123456789012",
  "arn:aws:s3:::app-logs-prod-123456789012",
]
bucket_ids  = [
  "app-logs-dev-123456789012",
  "app-logs-staging-123456789012",
  "app-logs-prod-123456789012",
]
kms_key_arn = null