Automating Storage Account and Service Principal Creation with Terraform

Effortlessly create an Azure Storage Account and Azure Service Principal for use in GitHub Actions by using Terraform.

Automating Storage Account and Service Principal Creation with Terraform
Automating Storage Account and Service Principal Creation with Terraform

When deploying Terraform using GitHub Actions there are certain perquisites you need to set up. You need to set up somewhere for your Terraform state file to be stored. And you need to have credentials that allow resources to be deployed to your cloud environment. 

When working with Azure those things are typically an Azure Service Principal and an Azure Storage account.

It can be tiresome and unreliable to create those things manually.

In this tutorial, I am going to show you how you can use Terraform to create those items and set up your GitHub Actions secrets

💡
Please note the code I am using was originally created by Ned Bellavance in 2021, I have taken it and updated it so that it works with the changes to Terraform and Azure that have happened since then. 

Prerequisites

Terraform

We’re going to create three Terraform files to help us create the necessary components within Azure and GitHub.  Those three files will be:

  • Terraform.tf - where we will define our Terraform configuration and providers required.
  • Main.tf - this is the body of our configuration and it will define all the parts we need. 
  • Variables.tf - this stores some of the configurable information we need to define for our resources. 

Let’s start creating these files.  We’ll start with variables.tf, open up your favourite code editor, I will be using VS Code. 

Create a new file, and save it in an appropriate folder and call it variables.tf.  Copy the following code into it: 

##
# Variables
##

variable "location" {
  type    = string
  default = "northeurope"
}

variable "naming_prefix" {
  type    = string
  default = "techielass"
}

variable "github_repository" {
  type    = string
  default = "github-actions-terraform"
}

variable "tag_usage" {
  type    = string
  default = "terraform-state"
}

variable "tag_owner" {
  type    = string
  default = "sarah"
}

This file is defining some variables that we need: 

  • Location: the Azure region we want to use for the resources we will be creating. 
  • Naming Prefix: this will go at the start of each of our resources to be created. 
  • GitHub repository: this is where we want our new secrets to be stored. This is the repository you will be using for a Terraform deployment of your architecture. 
  • Tag usage: this is what our Azure resources will be tagged with for easy identification. 
  • Tag owner: this is what our Azure resources will be tagged with for easy identification. 
💡
You will need to modify the values on each of these variables to suit your own requirements. 

The next file we will create is the terraform.tf file: 

##
# Terraform Configuration
##
terraform {

  required_version = ">=1.6.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.85.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "~> 2.47.0"
    }
    github = {
      source  = "integrations/github"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6.0"
    }
  }
}


##
# Provider configuration
##

provider "azurerm" {
  features {}
}

provider "random" {
  # Configuration options available
}

provider "azuread" {
  # Configuration options
}

provider "github" {
  # Configuration options
}

This file defines what the Terraform providers that we need, in this case we are using: 

💡
At time of writing, all versions listed are the latest. 

The next file we need to create is the main.tf file, which contains the bulk of our configuration:

##
# Locals
##
locals {
  resource_group_name    = "${var.naming_prefix}-${random_integer.sa_num.result}"
  storage_account_name   = "${lower(var.naming_prefix)}${random_integer.sa_num.result}"
  service_principal_name = "${var.naming_prefix}-${random_integer.sa_num.result}"
}

##
# Resources
##


## Create Azure Entra Service Principal ##

data "azurerm_subscription" "current" {}

data "azuread_client_config" "current" {}

resource "azuread_application" "gh_actions" {
  display_name = local.service_principal_name
  owners = [ data.azuread_client_config.current.object_id ]
}

resource "azuread_service_principal" "gh_actions" {
  client_id = azuread_application.gh_actions.client_id
  owners = [ data.azuread_client_config.current.object_id ]
}

resource "azuread_service_principal_password" "gh_actions" {
  service_principal_id = azuread_service_principal.gh_actions.object_id
}

## This assigns contributor role to the service principal ##

resource "azurerm_role_assignment" "gh_actions" {
  scope                = data.azurerm_subscription.current.id
  role_definition_name = "Contributor"
  principal_id         = azuread_service_principal.gh_actions.id
}

## Create an Azure resource group an an Azure Storage Account to store Terraform State ##

resource "random_integer" "sa_num" {
  min = 10000
  max = 99999
}


resource "azurerm_resource_group" "setup" {
  name     = local.resource_group_name
  location = var.location
    tags = {
    usage = var.tag_usage
    owner = var.tag_owner
  }
}

resource "azurerm_storage_account" "sa" {
  name                     = local.storage_account_name
  resource_group_name      = azurerm_resource_group.setup.name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  tags = {
    usage = var.tag_usage
    owner = var.tag_owner
  }
}

resource "azurerm_storage_container" "ct" {
  name                 = "terraform-state"
  storage_account_name = azurerm_storage_account.sa.name

}

## Store the Service Principal ID, Password, plus information about the storage account and Azure subscription in GitHub Secrets ##

resource "github_actions_secret" "actions_secret" {
  for_each = {
    STORAGE_ACCOUNT     = azurerm_storage_account.sa.name
    RESOURCE_GROUP      = azurerm_storage_account.sa.resource_group_name
    CONTAINER_NAME      = azurerm_storage_container.ct.name
    ARM_CLIENT_ID       = azuread_service_principal.gh_actions.client_id
    ARM_CLIENT_SECRET   = azuread_service_principal_password.gh_actions.value
    ARM_SUBSCRIPTION_ID = data.azurerm_subscription.current.subscription_id
    ARM_TENANT_ID       = data.azuread_client_config.current.tenant_id
  }

  repository      = var.github_repository
  secret_name     = each.key
  plaintext_value = each.value
}

There is a lot in this file, so let’s try and break it down and explain it. 

Variable definition

First we are defining some local variables to help us with the naming of our resources, they are combining the naming prefix variable we defined earlier and then generating a random number to add to the name of the resources. 

Gather Azure subscription information

We then have two data blocks gathering information from your Azure subscription so it can use the information to create the resources. 

The next section is creating the necessary Entra Application and Service Principal. 

Contributor access is then assigned to that Service Principal. 

Azure resource creation

And next we create an Azure resource group, Azure storage account and then a storage container inside that storage account.   It is creating a Standard LRS storage account. This section pulls in information from our variable file to define the Azure region used as the resources location and also the tags that will be attached to it. 

Create secrets in GitHub

The last section is taking the information from our generated resources and storing them inside our GitHub repository secret section.  Seven secrets will be created containing the following information: 

  • The storage account name
  • The resource group name where the storage account resides
  • The storage container name
  • The Service Principal ID
  • The Service Principal secret
  • The Azure subscription ID
  • The Azure Tenant ID
💡
If you would like a copy of these files you can find them stored within GitHub here

Deploy Terraform

Now we have the Terraform created we want to be able to deploy that to create our storage account and Service Principal. 

Create GitHub Personal Access Token

To do that we first need to create a GitHub personal access token (PAT) that Terraform can use for authentication to your GitHub account when it tries to create the secrets inside your repository.  

To generate a GitHub PAT token head over to https://www.github.com.

Click on your profile icon in the right-hand corner and select Settings.

GitHub Settings
GitHub Settings

Scroll down to Developer settings.

GitHub Developer Settings

Click on Personal access tokens then fine-grained tokens.

GitHub Personal Access Tokens
GitHub Personal Access Tokens

Then click on Generate new token.

You will be prompted with a bunch of questions, you need to give your token a name and set how long it will be active for. And then you need to start to define what repositories and permissions the token has. 

I like to scope it to the minimum required, by selecting the relevant repository and then only giving it Read/Write access to Secrets and Read access to Metadata

GitHub Personal Access Token permissions
GitHub Personal Access Token permissions

Once you’ve provided all the required answers click on the Generate token button. 

The next screen will display the token information, take a note of this as you’ll need it in the next step and it is only displayed once. 

Now that we have the PAT token, we need to head over to our browser and open https://shell.azure.com.  We are going to use that to deploy our Terraform creation files. 

When the Azure Cloud Shell launches, ensure you are using the Bash environment. 

Click on the Upload/Download icon and select Manage file share.

Azure Cloud Shell
Azure Cloud Shell

Click on Add Directory.

Azure Cloud Shell
Azure Cloud Shell

Give the directory a name, such as Terraform-Backend-configuration.

Once it has been created, click into it and then click on the Upload buttonUpload the three terraform files that you created earlier. 

Azure Cloud Shell
Azure Cloud Shell

With the files uploaded, switch back to your terminal window. 

Let's enter our GitHub PAT token key.

export GITHUB_TOKEN=PAT_TOKEN_VALUE

The next step we need to carry out is move our terminal to work within the directory where our Terraform is stored. 

cd clouddrive/Terraform-Backend-configuration/

We are now ready to initialise and apply the code.

terraform init
terraform apply -auto-approve

After a few minutes you will have an Azure Service Principal, Azure Storage Account, storage container deployed.  Along with all the required secrets created within your GitHub repository. 

GitHub Secrets
GitHub Secrets

You can now start to build your Terraform deployment files and GitHub Actions workflow using these secrets and Terraform backend infrastructure.

Conclusion

You can reuse this Terraform deployment as many times as you need, it’s a great way to quickly spin up the required bits to enable you to set up an Azure deployment within GitHub Actions quickly.  Great if you have a lot of repositories for different functions or are building a lot of demo environments.