An Introduction to Terraform Templates

A look at Terraform templates, providers, resources, and modules

NOV 12, 2020 | ABHISHEK HUGAR
undefined

Welcome back to our blog series on Terraform, where we break down the various components piece-by piece, for you to become an expert. In our current series, we’re going to discuss everything you need to know about resources, providers, and modules. Note: This blog is geared towards those who are familiar with the basics of Terraform. If you’re new to Infrastructure as Code, we suggest that you first read our previous post on "Terraform Tutorial Guides" to build a strong foundation before diving into the current content.

We believe that in order to understand Terraform templates, there are a number of subcategories to also understand. That’s why we included providers, resources, and modules in our explanation.

In this article, we’ll give you concise and actionable explanations to:

  • Terraform Templates
  • Defining Providers
  • Declaring Resources
  • An Overview of Modules

An Overview of Terraform Templates

A terraform template is a collection of files that, together, define the state of your infrastructure to be achieved. They include different configuration files such as variables, resources, and modules.

You can keep the terraform template files separately, or all under one configuration file--it’s usually your own choice.

Before you begin building any terraform templates, here are a few key terms and concepts to familiarize yourself with:

  • Blocks: These act like containers, and always begin with the name of the type of block, which can be a resource, variable, or provider. Depending on the type of block, the number of labels will be defined. The content is then defined within opening and closing curly brackets (‘{‘ and ‘}’).
  • Comments: To increase the readability of the code, you may always include comments to specify the function of any line of code. You may either use “#” or “//” to define the single line comments. Multi-line comments are written between “/*” and “*/”.
  • Arguments: These are the name and value pairs inside a block, where a value is assigned to a particular name. The name is called an “identifier,” which is followed by the equal to (=) sign and then a particular value or an expression is written on the right-hand side of the “=” sign. An identifier or name may contain a letter, a number, an underscore (_) or a dash (-), and should never start with a number.
  • Functions: There is support for pre-defined functions, which may be used within expressions in order to manipulate values. You can use functions by using function names, followed by writing values that need to be passed to the function between the opening and closing brackets (‘(‘ and ‘)’ respectively). However, you cannot define your own functions and are only able to use built-in ones. While it’s not possible to cover all of the built-in functions in this blog post, we encourage you to take time and study the pre-built ones provided by Terraform.

Terraform templates are always in a modular structure. In simpler terms, we can say that they’re set up in a file hierarchy, where each configuration file contains a name with the extension “.tf” (or in JSON format, “.tf.json”). Each configuration file needs to be UTF-8 encoded and is written as a block.

The below image indicates the general syntax of how we may write blocks:

Source: Configuration Language

Let’s take a look at the steps necessary to set up your configuration:


We’ve already covered the topic of defining and writing variable files in our previous blog, and will dive into the other steps below.

Defining Providers

When we say providers, we’re actually talking about the cloud or self-hosted service providers who enable all of your requirements based on your configuration files. When it comes to Terraform, providers are available as installable plugins, where each plugin is available in different versions and each version will offer its own set of resources, arguments that are accepted, and attributes that can be exported.

These installable plugins are automatically installed when you initialize your working directory, or in other words, when you run the “init” command. If you already have a working directory that has been initialized/applied, and you make changes to add a new provider, the new provider is automatically installed when you initialize the directory again. But, how can we specify which provider should be selected?

This specification is done under the “required_providers” block. This particular block is written inside the “terraform” block. Like all other blocks that we’ve seen, these blocks also start and end with an opening and closing curly brackets. The “required_providers” block contains a local name for the block, the URL or the address of the source of the provider, and an optional value specifying which version should be downloaded.

Let’s see an example of this:

terraform {
  required_providers {
    mycloud = {
      source = "mycorp/mycloud"
      version = "~> 1.0"
    }
  }
}

Source: Requirements Block

NOTE: Only 1 provider can be specified for each block.

A few providers will also need you to write configuration blocks before they’re used. These blocks will usually contain details such as the region and URLs. These can be written inside the “provider” block. It begins with the keyword “provider,” followed by the local name defined in the “required_providers” and between the curly brackets, you may write arguments that are always provider-specific.

For example:

provider "google" {
  project = "acme-app"
  region = "us-central1"
}

Source: Writing Configurations

NOTE: You may reference only those values that have been defined before the configuration, such as input variables. This means that the attributes exported by the resources cannot be used in the expressions.

Providers have meta-arguments that may be used:

Declaring Resources

To understand resources, you must first question, “What exactly are resources when it comes to Infrastructure?”. Well, they could be instances, storage space, DNS records, virtual networks, etc. So, you see now that resources are the basic requirements in your infrastructure. Terraform allows you to write blocks in your configuration to define all the resources that should be part of your infrastructure.

Resource blocks permit you to write configurations for the type of infrastructure object you will need. If it’s a new configuration that is being defined, then it is created on the “apply” command and the necessary “state file” is created. If it’s about updating a current object, then the state file is compared with the new object and necessary changes are updated.

The logical next step is to know how to write a resource block for a configuration.

Every block in this particular situation starts with the keyword “resource” and is followed by the type and then by a locally defined name. We then write the requirement configurations within the opening and closing curly brackets, “{“ and “}” respectively. Naming conventions specify that the resource name should begin with a letter or underscore (_) and contain only letters, digits, underscores and dashes.

Example:

resource "aws_instance" "web" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
}

Source: Resource Syntax

The “type” defined in the block is always the cloud provider that we’ll use to implement our infrastructure. Each provider will have their own types of configurations that can be used in the block between the curly brackets. We’ll talk more about the nuances of providers in a bit. However, at any stage, feel free to browse and take a look at the available providers and their documentation.

If you’re looking to access information regarding the current configuration in the same module, use the syntax below:

<TYPE_OF_RESOURCE>.<RESOURCE_NAME>.<ATTRIBUTE>

If you take into consideration the example block we’ve shown above, then to access an attribute “ami” in that block, you have to write a line as “aws_instance.web.ami”.

The following image shows a list of meta-arguments and their basic uses:

To keep this guide concise, we’ve decided to simply provide an overview of meta-arguments. If you’re looking to go more in-depth, you can find more resources here.


An Overview of Modules

When you imagine Infrastructure as Code, visualize all of the resources and variables in a written and readable format, inside of a particular container or blocks of lines of code. This particular container is what we call “modules.” If you’re already familiar with programming languages, then simply think of modules as playing the role of a function.

They hold all of the similar resources and variables required for your infrastructure configuration. Similar to functions, one module may call another and return values. As with any other programming languages and functions within it, even in Terraform, it’s mandatory to have at least 1 module called the “root module”. All of the “.tf” extension files under the working directory constitute the root module.
Reusability is another added advantage, where one module can be defined once and be called in the same configuration or even from other configurations.

You can create a new module by simply creating a new directory and placing your “.tf” configuration files inside them. These files can be accessed by Terraform either by using the local file path or by directly from remote storage, depending on where you have your files.

The below diagram shows the most commonly defined features/files inside a module:

You can include additional files in your directory:

  • README: A basic description of what the module does, where it can be used, and whether it nests any other modules. It can also be a markdown (.md) file
  • LICENSE: Indicates which license the module belongs to. It's important because most organizations prefer licensing before usage.
  • MODULES: If your module has child modules, nest it in the same directory.
  • EXAMPLES: A few examples to show the usage of the modules. It's also good to have a README file, along with the examples. Preferably place these under the root module.

To define your Terraform template, use this example file structure:

Source: Standard Structures

When a child module is called from the parent in this scenario, it means that you can include the child configurations within the parent, and assign values for input variables. It’s vital to remember that you need to include the “source” argument, where the value is the path of the callee.

Example:

module "servers" {
  source = "./app-cluster"

  servers = 5
}



Source: Configurations

NOTE:
For any action done on a module, which may be any addition, removal or modification, remember to initialize the working directory using the “init” command, and then apply the changes.

Although the calling module can’t access the attributes of the callee, we can declare output values for those arguments which are needed by the calling module. You can do this by assigning the output variable to a local variable, and using the keyword “module”: module.child_module_name.argument_name.

NOTE: There are already predefined modules as per the providers in the Terraform Registry, which you may call as you need.

Here are different meta-arguments available for modules and their uses:

As you can see, there are many tips and tricks when it comes to Terraform templates, modules, and resources. We hope that you found the above guide useful, and are continuing to learn and grow on your Terraform journey. If you’re looking for more support or resources, join our InfraCode Slack to gain valuable help from our community of experts and join the discussion about Infrastructure as Code.