Skip to content

How to deploy Azure resources from our DevOps standard Terraform

In this guide you will learn how to create the required credentials and authenticate using these with the Azure RM terraform provider to deploy resources to subscriptions in the University's Azure tenancy.

What you will need and what you will get

You will need:

  • A standard DevOps GCP project created via the the GCP Project Factory, with a meta-project and at least one environment.
    • You will need the unique ids for the terraform-deploy service accounts for each environment (which can be found in the web console, or via the gcloud iam service-acounts describe command.)
  • Access to a subscription in Azure.
    • Subscriptions must be created by the Servers and Storage team, they can be requested by sending an email to the team.
    • You will need at least "Reader" permissions for the subscription to see deployed resources.
    • You will need the "acronym" tag for the subscription, which is assigned by the servers and storage team. This can be seen under the "Tags" in the subscription's page in the Azure portal, or using the command: az tag list --resource-id /subscriptions/<Subscription UUID> | jq -r '.properties.tags.Acronym'

Emailing for resource creation

We hope that in the near future we will be able to move away from email-ops for initial subscription creation, but for now this will have to be requested manually.

You will get:

  • The ability to deploy Azure resources via logan/terraform or a gitlab CI deploy pipeline.

Case study

We will use the example of a service called "Punt Booker" which has existing resources in GCP, but now needs to deploy a Resource Group to Azure.

We'll assume that the following cloud resources already exist:

  • A GCP project environment for the service, in this case the development environment: punt-booker-devel-00001
  • A service account in this environments called: terraform-deploy@punt-booker-devel-00001.iam.gserviceaccount.com
    • This service account has the unique id: 123456
    • This service account is the one that is impersonated when running logan on the command line and when run in a gitlab deploy pipeline.
  • A subscription created in Azure, called uis-puntbooker-devel-001.
    • The subscription has the acronym tag: puntbooker.

In addition, we should have an infrastructure gitlab repository, which deploys resources to the GCP environments.

Create a federated deployer app

Open a Merge Request (MR) in the Entra ID Application Factory project. The Merge Request should contain:

  • A file under applications/production/ called punt-booker-devel-terraform.yaml with the following content:

    applications/production/punt-booker-devel-terraform.yaml
    type: api-client
    
    display_name: "uis-spn-puntbooker-dev"
    
    description: >-
      Development terraform deployer application, with federated access via the
      development terraform GCP service account.
    
    federated_google_service_account_unique_ids:
      # terraform-deploy@punt-booker-devel-00001.iam.gserviceaccount.com
      - "12345"
    

    Note that this file is under applications/production/. In this case, "production" means "the production deployment of Entra ID application factory".

    The display name here has a specific format, as requested by the Servers and Storage team. It should match: <inst>-spn-<acronym>-<env>. Here our institution is uis, the acronym is from the subscription tag and is puntbooker, and our environment is dev.

Do I need to include the email address in a comment?

No - but it may be useful in the future if you want to refer back to it.

Why does the display name need to match that format?

This has been specifically requested by Servers and Storage to help them manage the resources within Azure. In the future we would prefer to move to a more delegated model where the DevOps resources don't fall under that umbrella, but for now keeping to that format will assist the S&S team.

When you have opened the Merge Request, tag it teamCloud and alert the Cloud Team in their Teams channel. You or a reviewer can trigger a manual plan for the MR from the CI pipelines page of the MR. This can help verify that the format of your files is correct and that the things you expect to be created will be created.

Once merged and deployed, your federated access application will be created.

You can then check the app's UUID at the production apps list .

Request that the deployer apps are given access to the subscription

Now that the app has been created, with federated access from our GCP deploy service account, we need the app's Service Principal to be granted permissions to deploy resources to that subscription.

This must be requested via an email to the servers and storage team. In this message specify:

  • The app that needs the permissions, including its display name and UUID.
  • The subscription that it needs access to, also specifying the UUID of the subscription.
  • That the app requires "Contributor" permissions for that subscription.

In the future, we hope to be able to move this process away from a manual request to a more automated process.

Deploy resources to the Azure subscription

Now that everything is setup for the terraform-deploy service account to have federated access to a service principal in Azure with contributor permissions, we are ready to deploy resources.

Open an MR in the service's terraform infrastructure repository to add this new Azure resource.

Configure the azurerm terraform provider

In the versions.tf file add the azurerm required provider:

versions.tf
# versions.tf specifies minimum versions for providers and terraform.

terraform {
  # Keep this in sync with .gitlab-ci.yml and .logan.yaml.
  required_version = "~> 1.12"

  # Specify the required providers, their version restrictions and where to get them.
  required_providers {
    # Already existing providers should be present here
    # ...

    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.29"
    }
  }
}

Add some useful locals to the locals.tf file that we can refer to later:

locals.tf
# Project locals should already be present here
# ...

# Azure related locals
locals {
  # This must match the configuration of the federated credential in Azure.
  azure_federated_credential_audience = "api://AzureADTokenExchange"

  # Geographic location in which to create Azure resources.
  azure_location = "uksouth"

  # The id of the Azure tenancy
  azure_tenant_id = "49a50445-bdfa-4b79-ade3-547b4f3986e9" # UniversityOfCambridgeCloud.onmicrosoft.com

  azure_application_id = lookup({
    # These are the UUIDs of the federated access apps that have been granted the contributor
    # permissions in our subscription. The below UUID is an example.
    development = "00000000-0000-0000-0000-000000000001" # uis-spn-puntbooker-dev
    # We can leave these blank as we don't yet have staging and production apps, but if we add them
    # in the future they can then be filled in here.
    staging     = ""
    production  = ""
  }, terraform.workspace, "")

  # The id of the Azure subscription used to create resources.
  azure_subscription_id = "00000000-0000-0000-0000-000000002" # uis-puntbooker-devel-001
}

Then in the providers.tf file configure the provider (the google.impersonation provider should already be present in our standard deployments):

providers.tf
# This provider runs in the context of the person invoking Terraform (i.e. your personal @cam.ac.uk account).
# This is simply used to create tokens to impersonate other, more powerful, service accounts.
provider "google" {
  alias = "impersonation"
}

# ...
# The file will (most likely) already contain some other provider configuration
# ...

# Generate a token for the Azure Application Factory Service Principal which is used to drive the
# Azure AD API.
data "google_service_account_id_token" "azuread" {
  provider               = google.impersonation
  target_service_account = local.workspace_config.terraform_sa_email
  target_audience        = local.azure_federated_credential_audience
}

provider "azurerm" {
  tenant_id       = local.azure_tenant_id
  client_id       = local.azure_application_id
  subscription_id = local.azure_subscription_id

  use_oidc   = true
  oidc_token = data.google_service_account_id_token.azuread.id_token

  features {}
}

This configures the azurerm provider using google impersonation and federated access.

Create a brand new Azure resource

Now we can create new azure resources with the configured provider. In a new file (in our case called azure-example.tf) we can add:

azure-example.tf
resource "azurerm_resource_group" "example" {
  name     = "example-resources"
  location = local.azure_location
}

When we commit these changes, along with the above adding the provider configuration, and then open an MR we should see the gitlab-ci pipeline correctly plan to add this resource group.

As this is the development environment, we can deploy this resource directly from the MR, and should see it created within the Azure portal.

Summary

In this how to guide, you learned how to create a federated access deployer application in Entra, how to grant it contributor permissions, and then use it along with the existing terraform-deploy service account in GCP to authenticate to and deploy from the Azure terraform provider.