5 minute read

This post is part of my Terraform 101 series, exploring how MacAdmins can use Infrastructure as Code to manage Apple environments.

Previously: Getting Started

Up Next: Resources and Data Sources (Coming soon!)


In the last post, we set up a basic Terraform project and configured the axm provider to connect to Apple School or Business Manager.

That worked fine for a quick test, but we hardcoded sensitive information directly into our configuration files. API credentials, sensitive file paths, and other secrets should not be in version control.

In this post, we’ll clean that up by using variables, .tfvars files, and environment variables. These are essential for making your configurations reusable and secure.

Why Variables Matter

Terraform variables let you make your code flexible without hardcoding values. Think of them like placeholders: you define a variable once, and then reference it throughout your configuration.

Without variables, you’d have to manually edit credentials, region names, or identifiers in multiple files whenever they change. With variables, you just change one place.

Declare a Variable

In most programming languages, you need to define a variable before you can use it. For example, in Java, you would add something like int variable_name = value; before you could use variable_name in the code.

In Terraform, the concept is the same, but the syntax is different. We use the variable block.

When you define a variable, you are telling Terraform that a given variable_name is available for use. Optionally, you can add more data such as a type (string, integer, etc.), description, default value, and more.

Here’s what that looks like in practice:

variable "database_password" {
  description = "The password to use to connect to the database."
  type        = string
  sensitive   = True
}

The official documentation describes the variable block in detail. I recommend a quick glance to get familiar with the arguments that I did not list.

Defining Variables

Now that we know all about the variable block, let’s update our project. We’ll start by creating a new file called variables.tf within the same project directory we used in the last post.

touch variables.tf

Inside it, declare your variables:

variable "client_id" {
  description = "Apple Business Manager API Client ID"
  type        = string
}

variable "key_id" {
  description = "Apple Business Manager API Key ID"
  type        = string
}

variable "private_key_path" {
  description = "Path to the Apple Business Manager API private key (.pem) file"
  type        = string
  sensitive   = True
}

variable "scope" {
  default     = "business.api"
  description = "API scope: business.api or school.api"
  type        = string
}

📝 Note that this file does not contain the actual values of your variables. It only defines each variable’s metadata. We’ll provide the values soon.

The updated project directory should now contain:

terraform/
├── providers.tf
├── terraform.tf
└── variables.tf

Updating the Provider Configuration (providers.tf)

Before:

provider "axm" {
  client_id   = "BUSINESSAPI.abcdef12-3456-4789-abcd-ef1234567890"
  key_id      = "98765432-dcba-4321-9876-543210fedcba"
  private_key = file("/path/to/private_key.pem")
  scope       = "business.api"
}

After:

provider "axm" {
  client_id   = var.client_id
  key_id      = var.key_id
  private_key = file(var.private_key_path)
  scope       = var.scope
}

Now Terraform knows to look for these values when you run a command.

Providing Values

There are several ways to provide values for defined variables. Let’s go over the most common and secure options.

Command Line Flags

You can provide variables directly in your command line:

terraform apply -var "client_id=BUSINESSAPI.abcdef12-3456-4789-abcd-ef1234567890" -var "key_id=987654..."

This may be fine for quick tests, but not ideal for longer-term projects. It is cumbersome to maintain and the values can show up in shell history, causing security concerns. This approach is not recommended for anything sensitive.

TFVars File

Create a file named terraform.tfvars in your project directory. Terraform will automatically loads this file when you run terraform plan or terraform apply.

Inside it, add your unique values that we retreived from the Apple School or Business Manager console in Terraform 101: Getting Started.

client_id        = "BUSINESSAPI.abcdef12-3456-4789-abcd-ef1234567890"
key_id           = "98765432-dcba-4321-9876-543210fedcba"
private_key_path = "/Users/yourname/Documents/private_key.pem"
scope            = "business.api"

⚠️ Important: Add *.tfvars files to your .gitignore. These files should not be committed to version control.

echo "terraform.tfvars" >> .gitignore

Using .tfvars is a great approach. It allows you to have unique files for each environment. For instance, instead of terraform.tfvars, you could have two files names development.tfvars and production.tfvars. These files would not be auto loaded though. Terraform auto loads .tfvars files only when they are named terraform.tfvars or *.auto.tfvars. If you rename the file, you use the -var-file argument to specify the file to use.

terraform plan -var-file=development.tfvars
terraform apply -var-file=development.tfvars

Environment Variables

One of the coolest tidbits about variables is that Terraform automatically detects environment variables prefixed with TF_VAR_.

export TF_VAR_client_id="BUSINESSAPI.abcdef12-3456-4789-abcd-ef1234567890"
export TF_VAR_key_id="98765432-dcba-4321-9876-543210fedcba"
export TF_VAR_private_key_path="/Users/yourname/Documents/private_key.pem"

When you run Terraform, it picks these up automatically as long as they are declared. You do not need to pass them in manually.

Validating Variables

Terraform lets you define validation rules to catch bad input early. Each rule contains a condition and an error_message. For example:

variable "scope" {
  default     = "business.api"
  description = "API scope: business.api or school.api"
  type        = string

  validation {
    condition     = contains(["business.api", "school.api"], var.scope)
    error_message = "Scope must be either 'business.api' or 'school.api'."
  }
}

The logic above checks the value to make sure it contains one of two values: either business.api or school.api. This provider only allows those two values, so it makes sense to validate the variable to prevent failures. Each variable is going to be different and you may or may not have the need to validate the inputs.

If the condition fails, Terraform will show the designated error message and then stop processing.

I recommend that you update the variables.tf file with this new scope validation definition, but it’s not necessary.

Variable File Recap

Do not confuse terraform.tfvars files with variables.tf files. They are completely different and they serve different purposes.

  • variables.tf are files where all variables are declared.
  • terraform.tfvars are files where the variables are assigned a value.

Next Post

By moving sensitive information out of your configuration and into variables, you’ve made your project cleaner and more secure.

In the next post, we’ll start using resources and data sources. These are the primary building blocks of every Terraform project. We’ll create real objects through the Apple APIs and learn how Terraform reads existing ones.

Up Next: Resources and Data Sources (Coming soon!)

Comments