Terraform: Count

7 minute read

Description

Here is a basic example of using the count meta argument to deploy multiple resources. Note that it is generally preferred (see the ‘When to Use’ section) to use for_each which I will cover in a different post.

To Resolve:

  1. So I have main.tf with the following:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    
    terraform {
       required_providers {
    
          random = {
             source  = "hashicorp/random"
             version = "~>3.3.2"
          }
    
          azurerm = {
             source  = "hashicorp/azurerm"
             version = "~>3.10.0"
          }
    
       }
       required_version = "~>1.1.0"
    }
    
    provider "random" {
    }
    
    provider "azurerm" {
    client_id                  = var.client_id
    client_secret              = var.client_secret
    subscription_id            = var.subscription_id
    tenant_id                  = var.tenant_id
    skip_provider_registration = true
    features {}
    }
    
    variable "rg_count" {
       description = "(Optional) Number of Resource Groups to deploy."
       type        = number
       default     = 2
    }
    
    variable "tenant_id" {
       description = "(Required) Service Principal AD Tenant ID - Azure AD for terraform authentication."
       type        = string
    }
    
    variable "subscription_id" {
       description = "(Required) Azure Subscription Id used to connect to AzureRM provider."
       type        = string
    }
    
    variable "client_id" {
       description = "(Required) Service Principal App ID - Azure AD for terraform authentication."
       type        = string
    }
    
    variable "client_secret" {
       description = "(Required) Service Principal Client Secret - Azure AD for terraform authentication."
       type        = string
    }
    
    resource "random_string" "naming_convention_unique" {
       count   = var.rg_count
       length  = 5
       upper   = false
       lower   = true
       numeric = true
       special = false
    }
    
    resource "azurerm_resource_group" "rg" {
       count    = var.rg_count
       name     = "aa-dev-tx-test-${random_string.naming_convention_unique[count.index].result}"
       location = "westus"
       tags = {
          Owner       = "Automation Admin"
          CostCenter  = "100"
          EntAppname  = "Automation Admin Terraform POC"
          Environment = "tst"
          Contact     = "gerry@automationadmin.com"
       }
    }
    
    output "res_out_rg_name" {
       value = azurerm_resource_group.rg.*.name
    }
    
    output "res_out_rg_id" {
       value = azurerm_resource_group.rg.*.id
    }
       
    output "ids" {
       value = random_string.naming_convention_unique.*.result
    }
    
  2. Running our regular:

    1
    2
    3
    
    terraform init
    terraform plan -var-file="env.tfvars" -out="tf.plan"
    terraform apply -auto-approve -input=false ./tf.plan
    
    • I get the following output:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    ids = [
    "kxu7e",
    "tqoxw",
    ]
    res_out_rg_id = [
    "/subscriptions/my-subscription-guid/resourceGroups/aa-dev-tx-test-kxu7e",
    "/subscriptions/my-subscription-guid/resourceGroups/aa-dev-tx-test-tqoxw",
    ]
    res_out_rg_name = [
    "aa-dev-tx-test-kxu7e",
    "aa-dev-tx-test-tqoxw",
    ]
    
  3. So count works good with an integer, no surprise. Let’s see how it handles a list:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    # replace
    # variable "rg_count" {
    #    description = "(Optional) Number of Resource Groups to deploy."
    #    type        = number
    #    default     = 2
    # }
    
    # with:
    
    variable "rgs_to_create" {
    description = "(Optional) List of Resource Groups to deploy."
    type        = list(string)
    default     = ["management", "organization"]
    }
    
    # Then down in the resources, do:
    
    count = length(var.rgs_to_create)
    
  4. As expected, this shows that we will create 2 resource groups since the length of the rgs_to_create is 2.

  5. Lastly, I wanted to create a more advanced example, so I created this main.tf:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    
    terraform {
    required_providers {
    
       random = {
          source  = "hashicorp/random"
          version = "~>3.3.2"
       }
    
       azurerm = {
          source  = "hashicorp/azurerm"
          version = "~>3.10.0"
       }
    
    }
    required_version = "~>1.1.0"
    }
    
    provider "random" {
    }
    
    provider "azurerm" {
    client_id                  = var.client_id
    client_secret              = var.client_secret
    subscription_id            = var.subscription_id
    tenant_id                  = var.tenant_id
    skip_provider_registration = true
    features {}
    }
    
    variable "resource_group_name" {
    description = "(Optional) The name of a Resource Group"
    type        = string
    default     = "aa-dev-tx-test-2"
    }
    
    variable "tenant_id" {
    description = "(Required) Service Principal AD Tenant ID - Azure AD for terraform authentication."
    type        = string
    }
    
    variable "subscription_id" {
    description = "(Required) Azure Subscription Id used to connect to AzureRM provider."
    type        = string
    }
    
    variable "client_id" {
    description = "(Required) Service Principal App ID - Azure AD for terraform authentication."
    type        = string
    }
    
    variable "client_secret" {
    description = "(Required) Service Principal Client Secret - Azure AD for terraform authentication."
    type        = string
    }
    
    
    locals {
    resource_groups = [
       "aa-dev-tx-test-2",
       "aa-dev-tx-test-3",
       "aa-dev-tx-test-4"
    
    ]
    aa_config = [
       ["automation-1", "Basic", "some-value"],
       ["automation-2", "Basic", "some-value"],
       ["automation-3", "Basic", "some-value"]
    ]
    
    tst_tags = {
          Owner       = "Automation Admin"
          CostCenter  = "100"
          EntAppname  = "Automation Admin Terraform POC"
          Environment = "tst"
          Contact     = "gerry@automationadmin.com"
       }
    }
    
    resource "azurerm_resource_group" "rg" {
    count    = length(local.resource_groups)
    name     = local.resource_groups[count.index]
    location = "westus"
    tags     = local.tst_tags
    }
    
    resource "azurerm_automation_account" "example" {
    count               = length(local.resource_groups)
    name                = local.aa_config[count.index][0]
    location            = "westus"
    resource_group_name = local.resource_groups[count.index]
    sku_name            = local.aa_config[count.index][1]
    
    tags = local.tst_tags
    }
    
    output "rg_names" {
    value = azurerm_resource_group.rg.*.name
    }
    
    output "aa_names" {
    value = azurerm_automation_account.example.*.name
    }
    
  6. Which resulted in:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    Plan: 6 to add, 0 to change, 0 to destroy.
    
    Changes to Outputs:
    + aa_names = [
          + "automation-1",
          + "automation-2",
          + "automation-3",
       ]
    + rg_names = [
          + "aa-dev-tx-test-2",
          + "aa-dev-tx-test-3",
          + "aa-dev-tx-test-4",
       ]
    
  7. So this is not how I would do this in real life since it is almost always better to use for_each, but it does show how the count meta-argument works.

    • So what it does is creates a list object local.resource_groups which is a list of strings (type list(string)) with a length of 3.
    • It then creates a local.aa_config which is a list of lists ( type list(list(string)) ) with the same length of 3.
    • Since these happen to match up, what will happen is that local.resource_groups[0] will match up with local.aa_config[count.index][0] and local.aa_config[count.index][1] perfectly.
    • This works because both have the same amounts of elements. But again, I would use for_each as mentioned in the top description for the reason that it is hard to modify this. If you add a new element it will force you to destroy and recreate all of it instead of just adding that one element!
  8. If you noticed that local.aa_config[count.index][2] with the value of "some-value" was never used, good catch! This was on purpose to demonstrate that you don’t have to reference every value in a list if you don’t want to.

    • For example, you could have skipped local.aa_config[count.index][1] and referenced local.aa_config[count.index][2] if the values were switched like:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    aa_config = [
       ["automation-1", "some-value", "Basic"],
       ["automation-2", "some-value", "Basic"],
       ["automation-3", "some-value", "Basic"]
    ]
    
    # Then update sku name to be the third element [2] instead of the second [1]
    
    # sku_name            = local.aa_config[count.index][2]
    
    • It is real important with any language that you understand that they almost always start with [0] as the first element and then move on. So if something is in n position in a list, just internally think “to reference this I will use n - 1”. Example, to reference bob in list names = [ "jim", "bob", "anna"] you would do names[1] even though bob is in position 2.

    • Likewise, you can usually do backwards references like to reference anna it would be names[-1] which is always the last element in the array. You can step backwards and say names[-3] and that would get you jim but if you do names[-4] you would get an out of bounds type error which means you tried referencing something that doesn’t exist. Definitely play around with lists as they are used in many languages!

    • Also, a list of lists just takes it one step further so you when you reference local.aa_config[0] for example you end up with value ["automation-1", "some-value", "Basic"] so you then have to reference one step further to get the one you want. For example, if I want to get some-value in the middle then I would target local.aa_config[0][1] since [1] would be the second value in the list that was returned.

    • Again, spend some time working on learning this, it is used in many languages and is usually at the heart of most leet-code challenges used in interviews. See some python examples on my Github for example.

Comments