Getting started with Terraform and Azure Government

So, I’ve done quite a few posts recently on Azure Government, and specifically had a focus on infrastructure-as-code. I’ve had a lot of posts on bicep, but an equally powerful option is Terraform. In fact, in my experience, TerraForm has a much more robust feature set and is a fantastic option for managing solutions for larger organizations.

So what is TerraForm?

TerraForm is an open-source solution for building infrastructure-as-code. The repo and project is owned by a company called Hashicorp. The biggest selling point of TerraForm is that you can use the same language and tools to deploy infrastructure for Azure, AWS, and GCP.

Under the hood, TerraForm uses providers which are built in golang, and provide options for connecting these scripts to backend clouds.

The key feature for me, that makes TerraForm so powerful, is state management. When you are working with bicep or ARM templates, you are defining the the desired state of the cloud infrastructure, and when deployed it either creates or modifies resources accordingly. The problem with bicep / ARM templates is that they don’t manage the underlying state, so if the environment changes, from manual intervention for example, then it can get out of sync with the environment.

For example, if you have bicep that deploys an app service named “test-ui-app-service.” If I perform the following steps:

  1. Deploy the bicep template
  2. Change the name of the app service to “test-ui-va-app-service.”
  3. Deploy the bicep template again

After that, you will see 2 app services, one named “test-ui-app-service” and one named “test-us-va-app-service.”

This is because bicep doesn’t manage state and cannot reconcile the changes.

Terraform on the other hand does. When you perform a deployment in TerraForm, it will generate a change file, either locally or in the cloud, and this can be used to identify such changes.

TerraForm even provides an option to run a “plan” which enables you to see the differences before it makes them to the environment. This can be powerful, and provides a lot of great options to track changes before they are executed.

Where can I get started with TerraForm?

So if you want to get started with TerraForm, you can start by looking at the following documentation: Quickstart: Install and Configure Terraform.

But you can find the specific steps here as well:

wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

Now when it comes to Azure Government, there’s some additional steps involved, and those should be called out here. The first being that Terraform works in terms of providers, and you need to define a provide:

provider "azurerm" { 
  environment = "usgovernment"
  features {}
}

This denotes that you are using the Azure provider and the government cloud. Next lets define some resources to show how to work with terraform. Let’s create a resource group, and a storage account. Below is the contents of my “main.tf”

# Configure the Azure provider
provider "azurerm" { 
  environment = "usgovernment"
  features {}
}

resource "azurerm_resource_group" "hello-world" {
  name     = "hello-world"
  location = "usgovvirginia"
}

resource "azurerm_storage_account" "helloworld" {
  name                     = "helloworldstg"
  resource_group_name      = azurerm_resource_group.hello-world.name
  location                 = azurerm_resource_group.hello-world.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  min_tls_version          = "TLS1_2"
}

From there, I can run a terraform plan, which will show me what will happen if I apply this plan:

Terraform will perform the following actions:

  # azurerm_resource_group.hello-world will be created
  + resource "azurerm_resource_group" "hello-world" {
      + id       = (known after apply)
      + location = "usgovvirginia"
      + name     = "hello-world"
    }

  # azurerm_storage_account.helloworld will be created
  + resource "azurerm_storage_account" "helloworld" {
      + access_tier                      = (known after apply)
      + account_kind                     = "StorageV2"
      + account_replication_type         = "LRS"
      + account_tier                     = "Standard"
      + allow_blob_public_access         = false
      + enable_https_traffic_only        = true
      + id                               = (known after apply)
      + is_hns_enabled                   = false
      + large_file_share_enabled         = (known after apply)
      + location                         = "usgovvirginia"
      + min_tls_version                  = "TLS1_2"
      + name                             = "helloworldstg"
      + nfsv3_enabled                    = false
      + primary_access_key               = (sensitive value)
      + primary_blob_connection_string   = (sensitive value)
      + primary_blob_endpoint            = (known after apply)
      + primary_blob_host                = (known after apply)
      + primary_connection_string        = (sensitive value)
      + primary_dfs_endpoint             = (known after apply)
      + primary_dfs_host                 = (known after apply)
      + primary_file_endpoint            = (known after apply)
      + primary_file_host                = (known after apply)
      + primary_location                 = (known after apply)
      + primary_queue_endpoint           = (known after apply)
      + primary_queue_host               = (known after apply)
      + primary_table_endpoint           = (known after apply)
      + primary_table_host               = (known after apply)
      + primary_web_endpoint             = (known after apply)
      + primary_web_host                 = (known after apply)
      + resource_group_name              = "hello-world"
      + secondary_access_key             = (sensitive value)
      + secondary_blob_connection_string = (sensitive value)
      + secondary_blob_endpoint          = (known after apply)
      + secondary_blob_host              = (known after apply)
      + secondary_connection_string      = (sensitive value)
      + secondary_dfs_endpoint           = (known after apply)
      + secondary_dfs_host               = (known after apply)
      + secondary_file_endpoint          = (known after apply)
      + secondary_file_host              = (known after apply)
      + secondary_location               = (known after apply)
      + secondary_queue_endpoint         = (known after apply)
      + secondary_queue_host             = (known after apply)
      + secondary_table_endpoint         = (known after apply)
      + secondary_table_host             = (known after apply)
      + secondary_web_endpoint           = (known after apply)
      + secondary_web_host               = (known after apply)
      + shared_access_key_enabled        = true

      + blob_properties (known after apply)

      + identity (known after apply)

      + network_rules (known after apply)

      + queue_properties (known after apply)

      + routing (known after apply)

      + share_properties (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

I can then run the “terraform apply” to push those changes, once that is completed my resources will be deployed.

Like I said above, the real power in terraform is the ability to make changes. As you can see here, let’s update our “main.tf” to be the following:

# Configure the Azure provider
provider "azurerm" { 
  environment = "usgovernment"
  features {}
}

resource "azurerm_resource_group" "hello-world" {
  name     = "hello-world"
  location = "usgovvirginia"
}

resource "azurerm_storage_account" "helloworld" {
  name                     = "helloworldstg"
  resource_group_name      = azurerm_resource_group.hello-world.name
  location                 = azurerm_resource_group.hello-world.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  min_tls_version          = "TLS1_2"
}

resource "azurerm_storage_container" "test_1" {
  name                  = "test-1"
  storage_account_name  = azurerm_storage_account.helloworld.name
  container_access_type = "private"
}

Now if I run “terraform plan”, I get the following:

Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_storage_container.test_1 will be created
  + resource "azurerm_storage_container" "test_1" {
      + container_access_type   = "private"
      + has_immutability_policy = (known after apply)
      + has_legal_hold          = (known after apply)
      + id                      = (known after apply)
      + metadata                = (known after apply)
      + name                    = "test-1"
      + resource_manager_id     = (known after apply)
      + storage_account_name    = "helloworldstg"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Notice that it only mentions the storage container, not the storage account.

Hope this helps you get started with Terraform in an interesting way.