Infrastructure as Code with Terraform: Crafting Configuration Files for Resources Creation
Table of contents
- Introduction
- Understanding Terraform Configuration Files
- File Extensions and Structure
- Understanding Provider and Resource Blocks
- Terraform Resource Creation Templates
- Key Points in Using Terraform Templates
- Azure Resource Creation Template
- Example 1: Azure Storage Account
- Example 2: Azure Virtual Network
- AWS Resource Creation Template
- Example 1: AWS S3 Bucket
- Example 2: AWS EC2 Instance
- Google Cloud Resource Creation Template
- Example 1: GCP Compute Instance
- Example 2: GCP Storage Bucket
- Resource Referencing
- Using a Single Configuration File
- Modularity, Reusability, and Clarity
- Best Practices
- Conclusion
Introduction
Infrastructure as Code (IaC) tools enable you to manage and provision infrastructure using configuration files instead of relying on a graphical user interface. IaC facilitates the creation, modification, and management of infrastructure in a safe, consistent, and repeatable manner by allowing you to define resource configurations that can be versioned, reused, and shared.
Terraform, developed by HashiCorp, is a powerful IaC tool that allows you to describe resources and infrastructure in human-readable, declarative configuration files. It streamlines the lifecycle management of your infrastructure, ensuring consistency and reliability. Crafting configuration files effectively is key to maximizing its potential.
Advantages of Terraform
Terraform can manage infrastructure on multiple cloud platforms.
The human-readable configuration language helps you write infrastructure code quickly.
Terraform's state allows you to track resource changes throughout your deployments.
You can commit your configurations to version control to safely collaborate on infrastructure.
This article provides a general guide for beginners on how to build configuration files for resources creation using terraform with Azure, AWS and GCP providers example. Follow through and you are on your way to becoming a professional in using terraform for resources deployment.
Understanding Terraform Configuration Files
A Terraform configuration file is a declarative text file used to define infrastructure resources that Terraform will manage. Written in HashiCorp Configuration Language (HCL), it specifies the desired state of resources and their relationships. The configuration file acts as the blueprint for your infrastructure, enabling you to define, manage, and deploy resources consistently and reliably using Terraform.
Key Components of a Configuration File:
Provider Block:
Defines the cloud or service providers and their configurations (e.g., AWS, Azure).
Example:
provider "azurerm" { features {} }
Resource Block:
Represents the infrastructure resources to be created, such as virtual machines, networks, or storage accounts.
Example:
resource "azurerm_resource_group" "example" { name = "example-rg" location = "East US" }
Variable Block:
Declares dynamic inputs to parameterize configurations and improve reusability.
Example:
variable "location" { default = "East US" }
Output Block:
Outputs values like resource attributes after deployment for reference or integration with other systems.
Example:
output "resource_group_name" { value = azurerm_resource_group.example.name }
Data Block:
Fetches existing infrastructure information for use in the configuration.
Example:
data "azurerm_subscription" "current" {}
Terraform Block:
Sets project-wide configurations like backend storage for state files and required provider versions.
Example:
terraform { required_version = ">= 1.5.0" }
File Extensions and Structure
Terraform configuration files typically use the
.tf
extension.A single file (
main.tf
) can contain all blocks, but best practices suggest splitting into logical files (e.g.,provider.tf
,variables.tf
,outputs.tf
) for maintainability. There is also Terraform.tfvars file which contains variable values for different environments.
Understanding Provider and Resource Blocks
Providers block
The provider
block is used to configure the specific provider Terraform will use to interact with cloud platforms, SaaS services, or other APIs. Providers are plugins that enable Terraform to manage resources across a wide range of platforms, such as Azure, AWS, Google Cloud, Docker, Kubernetes, and many more. Every Terraform configuration must declare the required providers, allowing Terraform to download and use the appropriate plugins. Additionally, you can define multiple provider
blocks in a single configuration to manage resources across different platforms simultaneously, enabling seamless integration and unified infrastructure management.
The Required Providers
The required_providers
block is used to declare provider requirements in a Terraform configuration. It specifies the local provider name, its source location, and an optional version constraint. This block must be nested within the top-level terraform
block, which can also include other settings. Defining a version constraint for each provider in the required_providers
block is strongly recommended. While the version
attribute is optional, setting it helps enforce a specific provider version, ensuring stability. Without it, Terraform defaults to the latest provider version, which could introduce breaking changes.
In example 1a below, the local name is docker, source is “kreuzwerker/docker" and version is "~> 3.0.1"
while in example 1b, the local name is azurerm, source is “hashicorp/azurerm" and version is "~> 3.0"
# Example 1a: Docker provider declaration
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.1"
}
}
}
# Example 1b: Azure provider declaration
terraform {
required_version = ">=1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
}
}
Referencing Providers
In Terraform configurations, providers are always referenced by their local names outside of the required_providers
block. In example 2a below , the configuration declares azurerm
as the local name for hashicorp/azurerm
, then referenced that local name when configuring the provider:
# Example 2a: Referencing Azure providers
terraform {
required_version = ">=1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
}
}
provider "azurerm" {
features {}
}
#Example 2b: Referencing AWS providers
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "us-west-2"
}
# Example 2c: Referencing GCP providers
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "6.8.0"
}
}
}
provider "google" {
project = "<PROJECT_ID>"
region = "us-central1"
zone = "us-central1-c"
}
Resource Block
Resource blocks have two strings before the block
1. resource type
2. resource name.
Terraform Resource Block Format
resource "<provider>_<resource_type>" "<resource_name>" {
# Required attributes
name = "<resource_name>"
location = "<resource_location>"
resource_group_name = "<resource_group>"
# Optional attributes
tags = {
Environment = "Development"
Owner = "YourName"
}
# Nested blocks (if applicable)
some_nested_block {
attribute_1 = "value"
attribute_2 = "value"
}
}
General Guidelines
Resource Identifier:
The first string<provider>_<resource_type>
specifies the provider and resource type e.g.,azurerm_virtual_network
.Resource Name:
The<resource_name>
is an internal identifier you use to reference the resource in the code. e.g., vnetAttributes:
First Include required attributes (e.g.,name
,location
), followed by optional ones (e.g.,tags
, nested blocks).Tags:
Adding tags is a best practice for organizing and identifying resources.Nested Blocks:
Some resources include nested configurations, such assubnet
in a virtual network.Variables and Locals:
Use variables or local values for reusable and parameterized code:Outputs:
Export important values for other modules or users:# Example 3a: Resource declaration 1 resource "azurerm_resource_group" "rg" { name = "myResourceGroup" location = "westus2" }
In Example 3a above, the resource type is
azurerm_resource_group
, the resource name isrg
, the name attribute ismyResourceGroup
and location attribute iswestus
.Together, the resource type and resource name form a unique ID for the resource. The ID for the resource in the example here is
azurerm_resource_group.rg
.Resource blocks contain arguments enclosed within {} which are used to configure the resource. Arguments to include depend on the resource. For the example in context, the arguments are the resource name and location.
# Example 3b: Resource declaration 2
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "us-west-2"
}
resource "aws_instance" "app_server" {
ami = "ami-830c94e3"
instance_type = "t2.micro"
tags = {
Name = "ExampleAppServerInstance"
}
}
In Example 3b above, the resource type is
aws_instance
and the name isapp_server
.The ID for the AWS instance is
aws_instance.app_server
The resource blocks arguments included are ami ,instance_type, tags and Name .
# Example 3c: Resource declaration 3
resource "azurerm_resource_group" "rg" {
name = "myResourceGroup"
location = "westus2"
}
resource "azurerm_storage_account" "storage_account" {
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
name = "myStorageAccount"
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
}
In Example 3c above, there are two resources: Resource group and storage.
For storage, the resource type is
azurerm_storage_account
and the resource name isstorage_account
forming an IDazurerm_storage_account.storage_account
for the resource.The Arguments for storage here is resource_group_name, location, name, account_tier, account_replication_type and account_kind.
Terraform Resource Creation Templates
Templates in Terraform are highly adaptable, allowing you to create any resource by simply modifying the a few things to match your specific requirements. Reliable sources for Terraform resource templates vary based on the resource type and cloud provider. A great starting point is the Terraform Registry and official documentation from cloud providers, as these resources are regularly updated and follow best practices. Once you have found a template, you can customize it to meet your project’s unique needs. Below is a list of recommended sources for Terraform templates:
- Terraform Registry
Terraform registry is the official source for verified Terraform modules and providers. It includes:
Modules: Prebuilt configuration for common use cases.
Documentation: Example for individual resource blocks.
- GitHub
Search for Terraform templates and examples on GitHub. Many organizations and developers share their Terraform repositories.
- Cloud Provider Documentation
Cloud providers like AWS, Azure and GCP have Terraform-specific examples in their documentation
- Community Modules
The Terraform community contributes to reusable modules you can find and customize for your needs:
- HashiCorp Learn Platform
HashiCorp Learn provides step-by-step guides and tutorials, including ready-to-use templates for
Infrastructure on AWS, Azure, and GCP
Kubernetes deployment
Docker configurations
Blog and Tutorials
Several blogs and tutorials sites publish Terraform examples and best practices
Key Points in Using Terraform Templates
Provider Block: This block is required . Here you specify the provider (e.g., AWS, Azure, GCP).
Resource Naming: Here
the
<resource type>
is usually named by using underscore (_) to combine the provider name (e.g. azurerm
) and the specific resource (e.g virtual_network
)e.g
.azurerm
_virtual_network
.The customizable part is the
<resource_name>
. You can edit it by replacing it with your desired internal name for referencing ite.g., vnet
.
Attributes:
Include mandatory attributes such as
name
,location
, andresource_group_name
.The name attribute is also customizable. You can edit it by replacing it with your desired name
e.g., my-vnet
.Add optional attributes or nested blocks as needed.
Tags:
Always use tags
Always use tags for resource identification and cost management.
Outputs:
- Define outputs to expose resource details like IDs or endpoints for other modules or debugging.
Terraform Commands:
Initialize:
terraform init
Preview changes:
terraform plan
Apply changes:
terraform apply
Azure Resource Creation Template
Below is a general template for creating a resource using Terraform with Azure (azurerm) as the provider:
# Provider configuration
provider "azurerm" {
features {} # Required block for the AzureRM provider
}
# Resource definition
resource "azurerm_<resource_type>" "<resource_name>" {
# Required attributes
name = "<resource_name>"
location = "<resource_location>" # Example: "East US"
resource_group_name = "<resource_group_name>" # Existing resource group
# Optional attributes
tags = {
Environment = "Development" # Example tags
Owner = "YourName"
}
# Nested blocks (if applicable)
<nested_block_name> {
attribute_1 = "value"
attribute_2 = "value"
}
}
# Outputs (Optional)
output "<output_name>" {
value = "<resource_reference>" # Example: azurerm_<resource_type>.<resource_name>.id
}
Example 1: Azure Storage Account
# Provider configuration
provider "azurerm" {
features {} # Required block
}
# Storage account resource
resource "azurerm_storage_account" "project" {
name = "project-storaccount12" # Must be globally unique
resource_group_name = "myResourceGroup"
location = "East US"
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
Environment = "Production"
Owner = "Celestina Odili"
}
}
# Output: Storage account primary endpoint
output "storage_account_primary_endpoint" {
value = azurerm_storage_account.example.primary_blob_endpoint
}
Example 2: Azure Virtual Network
# Provider configuration
provider "azurerm" {
features {}
}
# Virtual network resource
resource "azurerm_virtual_network" "VNet" {
name = "vnet1"
location = "East US"
resource_group_name = "myResourceGroup"
address_space = ["10.0.0.0/16"]
tags = {
Environment = "Development"
Owner = "Celestina Odili"
}
# Optional nested subnet block
subnet {
name = "db-subnet"
address_prefix = "10.0.1.0/24"
}
}
# Output: Virtual network ID
output "vnet_id" {
value = azurerm_virtual_network.VNet.id
}
AWS Resource Creation Template
Here is a general template for creating a resource using Terraform with AWS as the provider:
# Provider configuration
provider "aws" {
region = "<region>" # Example: "us-east-1"
}
# Resource definition
resource "aws_<resource_type>" "<resource_name>" {
# Required attributes
name = "<resource_name>" # Name of the resource
# Optional attributes
tags = {
Environment = "Development" # Example tags
Owner = "YourName"
}
# Nested blocks (if applicable)
<nested_block_name> {
attribute_1 = "value"
attribute_2 = "value"
}
}
# Outputs (Optional)
output "<output_name>" {
value = "<resource_reference>" # Example: aws_<resource_type>.<resource_name>.id
}
Example 1: AWS S3 Bucket
# Provider configuration
provider "aws" {
region = "us-east-1"
}
# S3 bucket resource
resource "aws_s3_bucket" "example" {
bucket = "example-bucket-name" # Must be globally unique
acl = "private" # Access Control List
tags = {
Environment = "Production"
Owner = "Celestina Odili"
}
# Enable versioning
versioning {
enabled = true
}
# Lifecycle rules
lifecycle_rule {
id = "delete-old-versions"
enabled = true
noncurrent_version_expiration {
days = 30
}
}
}
# Output: S3 bucket ARN
output "s3_bucket_arn" {
value = aws_s3_bucket.example.arn
}
Example 2: AWS EC2 Instance
# Provider configuration
provider "aws" {
region = "us-west-2"
}
# EC2 instance resource
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0" # Example AMI ID
instance_type = "t2.micro"
tags = {
Environment = "Development"
Owner = "Celestina Odili"
}
# Optional: Add a key pair for SSH access
key_name = "example-key"
# Optional: Associate a security group
vpc_security_group_ids = ["sg-0123456789abcdefg"]
}
# Output: EC2 instance public IP
output "instance_public_ip" {
value = aws_instance.example.public_ip
}
Google Cloud Resource Creation Template
Here is a general template for creating a resource using Terraform with Google Cloud Platform (GCP) as the provider:
# Provider configuration
provider "google" {
project = "<project_id>" # Your GCP project ID
region = "<region>" # Example: "us-central1"
}
# Resource definition
resource "google_<resource_type>" "<resource_name>" {
# Required attributes
name = "<resource_name>" # Name of the resource
region = "<resource_region>" # If applicable
# Optional attributes
labels = {
Environment = "Development" # Example labels
Owner = "YourName"
}
# Nested blocks (if applicable)
<nested_block_name> {
attribute_1 = "value"
attribute_2 = "value"
}
}
# Outputs (Optional)
output "<output_name>" {
value = "<resource_reference>" # Example: google_<resource_type>.<resource_name>.id
}
Example 1: GCP Compute Instance
# Provider configuration
provider "google" {
project = "your-gcp-project-id"
region = "us-central1"
}
# Compute instance resource
resource "google_compute_instance" "example" {
name = "example-instance"
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-11" # Predefined image
}
}
network_interface {
network = "default"
access_config {
# Ephemeral external IP
}
}
labels = {
Environment = "Production"
Owner = "Celestina Odili"
}
metadata = {
ssh-keys = "user:ssh-rsa AAAAB3NzaC1..."
}
}
# Output: Compute instance external IP
output "instance_external_ip" {
value = google_compute_instance.example.network_interface[0].access_config[0].nat_ip
}
Example 2: GCP Storage Bucket
# Provider configuration
provider "google" {
project = "your-gcp-project-id"
region = "us-central1"
}
# Storage bucket resource
resource "google_storage_bucket" "example" {
name = "example-storage-bucket" # Must be globally unique
location = "US"
storage_class = "STANDARD"
labels = {
Environment = "Development"
Owner = "Celestina Odili"
}
versioning {
enabled = true
}
lifecycle_rule {
action {
type = "Delete"
}
condition {
age = 30
}
}
}
# Output: Bucket URL
output "bucket_url" {
value = google_storage_bucket.example.url
}
Resource Referencing
In Terraform, resource referencing is the process of connecting resources by passing attributes or outputs from one resource to another within the same configuration. This is key to building dynamic and dependent infrastructure, as it allows resources to interact with each other seamlessly.
Format for Referencing Resources
resource_type.resource_name.attribute
resource_type
: The type of resource (e.g.,aws_instance
,azurerm_storage_account
).resource_name
: The unique name you gave the resource in the configuration. (e.g.,rg,
storage_account
,app_server
).attribute
: The specific property or output of the resource you want to reference (e.g.,id
,name
,ip_address
).
Example
Here is an example of referencing in Terraform:
Creating an Azure Resource Group and Storage Account
resource "azurerm_resource_group" "project" {
name = "projectRG"
location = "East US"
}
resource "azurerm_storage_account" "project" {
name = "project-storageacct"
resource_group_name = azurerm_resource_group.project.name
location = azurerm_resource_group.project.location
account_tier = "Standard"
account_replication_type = "LRS"
}
Explanation
The
azurerm_storage_account
resource references theazurerm_resource_group
resource for:resource_group_name
: Refers toazurerm_resource_
group.project.name
.location
: Refers toazurerm_resource_group.project.location
.
This ensures that the storage account will always use the same resource group and location as defined in the resource group resource.
Using a Single Configuration File
Using a single configuration file in Terraform is ideal for small projects, quick prototyping, or simple setups because it reduces complexity, simplifies management, and speeds up configuration. It works well when all resources are logically related. However, as the infrastructure grows or when working in teams, a single file becomes harder to maintain, debug, and scale, making it unsuitable for larger, complex projects.
The example below is a Terraform configuration that deploys three Azure resources: a resource group, a virtual network, and a storage account in a single main.tf file.
# Configure the Azure provider
provider "azurerm" {
features {}
}
# Resource Group
resource "azurerm_resource_group" "project" {
name = "projectRG"
location = "East US"
}
# Virtual Network
resource "azurerm_virtual_network" "vnet" {
name = "project-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.project.location
resource_group_name = azurerm_resource_group.project.name
}
# Storage Account
resource "azurerm_storage_account" "projectStorage" {
name = "projectstorage123" # Must be globally unique
resource_group_name = azurerm_resource_group.project.name
location = azurerm_resource_group.project.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
environment = "Development"
}
}
# Outputs
output "resource_group_name" {
value = azurerm_resource_group.project.name
}
output "vnet_name" {
value = azurerm_virtual_network.vnet.name
}
output "storage_account_name" {
value = azurerm_storage_account.projectStorage.name
}
Modularity, Reusability, and Clarity
Best practices recommend splitting configurations into logical files to enhance modularity, reusability, and collaboration. While a single file is convenient for beginners or lightweight use cases, a multi-file structure is better for long-term maintainability and team workflows. If you want to ensure modularity, reusability, and clarity in your Terraform configurations, you need to structure your configuration files. It can be broken into provider.tf, main.tf, variable.tf, output.tf files.
Below is example of a multi Terraform configuration that deploys three Azure resources: a resource group, a virtual network, and a storage account. The configuration is divided into four files: provider.tf
, main.tf
, variables.tf
, and outputs.tf
.
1. provider.tf
(Provider Configuration)
provider "azurerm" {
features {} # Required block for the AzureRM provider
}
2. variables.tf
(Variable Definitions)
variable "resource_group_name" {
description = "The name of the resource group."
default = "projectRG"
}
variable "location" {
description = "The Azure region where resources will be created."
default = "East US"
}
variable "vnet_name" {
description = "The name of the virtual network."
default = "project-vnet"
}
variable "vnet_address_space" {
description = "The address space for the virtual network."
default = ["10.0.0.0/16"]
}
variable "storage_account_name" {
description = "The name of the storage account."
default = "projectstorage123" # Must be globally unique
}
3. main.tf
(Resource Definitions)
# Resource Group
resource "azurerm_resource_group" "project" {
name = var.resource_group_name
location = var.location
}
# Virtual Network
resource "azurerm_virtual_network" "vnet" {
name = var.vnet_name
address_space = var.vnet_address_space
location = azurerm_resource_group.project.location
resource_group_name = azurerm_resource_group.project.name
tags = {
Environment = "Development"
Owner = "Celestina Odili"
}
}
# Storage Account
resource "azurerm_storage_account" "projectStorage" {
name = var.storage_account_name
resource_group_name = azurerm_resource_group.project.name
location = azurerm_resource_group.project.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
Environment = "Development"
Owner = "Celestina Odili"
}
}
4. outputs.tf
(Output Definitions)
# Output: Resource Group Name
output "resource_group_name" {
description = "The name of the created resource group."
value = azurerm_resource_group.project.name
}
# Output: Virtual Network ID
output "vnet_id" {
description = "The ID of the created virtual network."
value = azurerm_virtual_network.vnet.id
}
# Output: Storage Account Primary Endpoint
output "storage_account_primary_endpoint" {
description = "The primary endpoint of the storage account."
value = azurerm_storage_account.projectStorage.primary_blob_endpoint
}
Best Practices
Use Outputs for Cross-Module References: If you are working across modules, use outputs to expose attributes.
output "resource_group_name" { value = azurerm_resource_group.project.name }
Avoid Hardcoding: Use references instead of hardcoding values to make your configuration more dynamic and reusable.
Use Terraform’s
depends_on
for Explicit Dependencies: While Terraform typically determines dependencies automatically, sometimes you need to specify them explicitly:depends_on = [azurerm_resource_group.project]
Follow Terraform's
terraform plan
: Always runterraform plan
beforeterraform apply
to ensure your references are resolving correctly.
Conclusion
Infrastructure as Code (IaC) with Terraform provides a robust and efficient approach to managing infrastructure, enabling consistency, scalability, and automation. By adhering to general guidelines for resource creation such as organizing configurations into modular files, using input variables for flexibility, and implementing best practices, you can ensure a structured and maintainable infrastructure setup. Terraform's declarative nature and support for multiple providers make it a powerful tool for both small-scale projects and complex, multi-cloud environments.
Check What is Infrastructure as Code with Terraform for more.