For this part of the series I want to talk a bit about the grammar and syntax of Terraform, how to reference existing resources, create new resources, and use variables.
Below is an example Terraform file using native syntax, which was designed to be human readable. You can get more details on all of this from the Terraform Documentation, but what I want to draw attention to here are the different block types like Terraform, Provider, Variable, Data, Locals, Resource, etc.
terraform {
required_version = ">= 1.3.9, < 2.0.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.40.0"
}
}
}
provider "azurerm" {
features {}
}
variable "rg_name" {
type = string
default = "DBAtlas"
description = "Name of the Resource group in which to deploy service objects"
}
variable "db_admin_login" {
type = string
sensitive = true
description = "Admin User Name"
}
variable "db_admin_pwd" {
type = string
sensitive = true
description = "Admin Password"
}
data "azurerm_resource_group" "dbatlas" {
name = var.rg_name
}
locals {
tags = {
environment = "Production"
repo = "Terraform\\AzureDBExample"
source = "terraform"
createdby = "[email protected]"
}
}
resource "azurerm_mssql_server" "exampleserver" {
name = "a-clever-and-meaningful-server-name"
resource_group_name = data.azurerm_resource_group.dbatlas.name
location = data.azurerm_resource_group.dbatlas.location
version = "12.0"
administrator_login = var.db_admin_login
administrator_login_password = var.db_admin_pwd
minimum_tls_version = "1.2"
public_network_access_enabled = true
tags = merge(local.tags, {})
}
resource "azurerm_mssql_database" "exampledb" {
name = "a-good-db-name"
server_id = azurerm_mssql_server.exampleserver.id
collation = "SQL_Latin1_General_CP1_CI_AS"
license_type = "LicenseIncluded"
max_size_gb = 1
read_scale = false
sku_name = "Basic"
zone_redundant = false
storage_account_type = "Local"
ledger_enabled = false
tags = merge(local.tags, {})
}
The Terraform block can be used for things like requiring certain versions of Terraform, configuring a backend store your state file in, and specifying provider requirements.
terraform {
required_version = ">= 1.3.7, < 2.0.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.40.0"
}
}
}
The Provider block, this is used to configure each specific provider and also tells Terraform what to install so that they can be used. I think of providers like plugins that allow you to interact with different systems – in this case we are using the AzureRM provider, which allows us to manage Microsoft Azure using the Azure RM APIs via Terraform.
provider "azurerm" {
features {}
}
The Variable block, or more succinctly input variables, allows you to specify parameters that make your code more composable and reusable.
variable "rg_name" {
type = string
default = "DBAtlas"
description = "Name of the Resource group in which to deploy service objects"
}
variable "db_admin_login" {
type = string
sensitive = true
description = "Admin User Name"
}
variable "db_admin_pwd" {
type = string
sensitive = true
description = "Admin Password"
}
The Data block allows you to load or query data from the API for existing resources including things that have been defined outside of Terraform or defined by other Terraform configurations. In this case an already existing resource group.
data "azurerm_resource_group" "dbatlas" {
name = var.rg_name
}
The Locals block is similar to the Variable block, generally these are often referenced values or expressions to help keep your code clean and orderly. In this case I am defining a base set of metadata tags I want to add to all my resources.
locals {
tags = {
environment = "Production"
repo = "Terraform\\AzureDBExample"
source = "terraform"
createdby = "[email protected]"
}
}
Finally we get to the Resouce block, the main attraction. Each of these blocks describes 1 or more infrastructure objects that you want Terraform to deploy and manage. Resource(s) are the backbone of your Terraform and all the other blocks are playing a supporting role to this.
azuread_administrator {
login_username = "Azure SQL Admin"
object_id = "1445b8e3-ec39-4ea1-920d-220553c4c267"
azuread_authentication_only = false
}
tags = merge(local.tags, {})
}
resource "azurerm_mssql_database" "exampledb" {
name = "a-good-db-name"
server_id = azurerm_mssql_server.exampleserver.id
collation = "SQL_Latin1_General_CP1_CI_AS"
license_type = "LicenseIncluded"
max_size_gb = 1
read_scale = false
sku_name = "Basic"
zone_redundant = false
storage_account_type = "Local"
ledger_enabled = false
tags = merge(local.tags, {})
}
So hopefully that gives you a good primer and understanding concerning Terraform and its syntax, as well as how we can use it to deploy and manage resources. We did not discuss all the blocks, there are more such as Modules and Out Values, that you can explore as you get more proficient and comfortable with Terraform but are outside the scope of this blog post. Also I would like to note my preference, and those or most people using Terraform, is to separate many of these blocks out into there own files to assist in maintainability. You can of course put everything inside of one file, and there are many opinions on the ways to structure your Terraform projects but I will leave it up to the reader to do their own research on this and develop a practice that works best for them.
So in Part 1 we covered getting tooling installed and other configuration steps. In this part we went over understanding Terraform syntax. In the final part of this series I hope to go over actual examples of deploying resources to Azure using Terraform.