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.
Prerequisites
- Azure subscription
- GitHub account
- Basic understanding of Terraform
- Code editor such as VS Code
- A GitHub repository
- Terraform installed on your local machine
- Azure CLI installed on your local machine
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.
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:
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
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.
Scroll down to Developer settings.
Click on Personal access tokens then fine-grained 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.
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.
Click on Add Directory.
Give the directory a name, such as Terraform-Backend-configuration.
Once it has been created, click into it and then click on the Upload button. Upload the three terraform files that you created earlier.
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.
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.