I love getting to a point with Infrastructure as Code (IaC) where not only are the resources reproducable, but also encoding good security and utilisation of cloud resources into the contents. Firstly, support in Azure Storage for Active Directory access control went GA and utilising this over an access key is one of those security considerations that seems could be automated. Secondly, managed identities are a fantastic way to get the power of Azure Active Directory without the process of keeping secrets and other management secure.
My tool of choice in Azure has been Azure Resource Manager (ARM) templates, but needing to do this across GCP as well these days, I’ve come back to Terraform as a great tool for IaC templates and a consistent tool across many resources, providers etc. Two resources to be aware of is the Terraform Azure Provider docs, but also resources are still created in ARM so the ARM Template Reference is also a required resource to determine exactly what might be acceptable for certain parameters.
What we’ll create
- Compute: Azure App Service
- Managed Identity
- Storage
- Container
- Blob
- Role Assignment: Storage blob data reader for our managed identity
- Application to utilise managed identity to read blob object
Prerequisites
- Have Terraform installed locally
- I’m using Terraform authentication from the Azure CLI and will assume you have the Azure CLI installed and logged in
- You will also have to have an Azure subscription to be able to deploy into
Terraform
All azure resources need a resource group so we’ll start by creating a main.tf
with two variables and the resource group itself. Nothing too exciting here, but we’ll use these in later resources.
variable "app_name" {
type = string
description = "The common name to use for resources"
default = "tf-az-roles"
}
variable "environment" {
type = string
description = "The deployment environment description"
default = "dev"
}
resource "azurerm_resource_group" "test" {
name = "${var.app_name}-rg"
location = "Australia East"
tags = {
environment = var.environment
}
}
App Service
The app service and app hosting plan are created here. They’re using locations aligned with the containing resource group and a free tier. The block of interest for our purposes is the identity
block which creates a managed identity for us. The terraform docs for the identity are quite good and outline that we can utilise this later using azurerm_app_service.test.identity.0.principal_id
.
resource "azurerm_app_service_plan" "test" {
name = "${var.app_name}-appserviceplan"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
sku {
tier = "Free"
size = "F1"
}
}
resource "azurerm_app_service" "test" {
name = "${var.app_name}-app-service"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
app_service_plan_id = azurerm_app_service_plan.test.id
identity {
type = "SystemAssigned"
}
}
Storage
One big advantage of terraform is that we can create more than just the parent resource: here we will also create a container and blob in our storage account. A great way to have all PaaS resources correctly created and can simplify our codebase by assuming they exist versus creating them at runtime. For our purposes of using RBAC, there’s nothing special here from any other deployment of a storage account.
resource "azurerm_storage_account" "test" {
name = "${replace(var.app_name, "-", "")}storageaccount"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_storage_container" "test" {
name = "${var.app_name}-container"
storage_account_name = azurerm_storage_account.test.name
resource_group_name = azurerm_resource_group.test.name
}
resource "azurerm_storage_blob" "test" {
name = "hello.txt"
resource_group_name = azurerm_resource_group.test.name
storage_account_name = azurerm_storage_account.test.name
storage_container_name = azurerm_storage_container.test.name
type = "block"
content_type = "application/text"
source = "hello.txt"
}
Roles and Assignments
Finally our managed identity gets to do something: we’re going to assign it to a rule within our resource group scoped to blob data reader. This is a built in role and others can be found at https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#storage-blob-data-reader. It’s worth noting that either the role_definition_name
or the role_definition_id
are needed and are mutually exclusive. The name seems easier to read and communicate to others, but there maybe a case were the role GUID may be more to your benefit.
// https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#storage-blob-data-reader
resource "azurerm_role_assignment" "test" {
role_definition_name = "Storage Blob Data Reader"
scope = azurerm_storage_account.test.id
principal_id = azurerm_app_service.test.identity[0].principal_id
}
With this addition, our managed identity should now have permissions scoped to read only within this storage account.
Reader web app
We’ll create a very bare bones ASP.NET Core Web API with a single endpoint that returns our blob’s content. This will be sufficient to demonstrate using our managed identity to get an access token and subsequently using that access token to read from storage.
The following commands can be run from terminal and create our web api and add two packages: one used to simplify getting an access token using our managed identity and the second Azure storage libraries.
$ dotnet new webapi -o app
$ cd app
$ dotnet add package Azure.Identity
$ dotnet add package Azure.Storage.Blobs
From our template, we’ll modify the ValuesController
to the content below. Deleting all the endpoints apart from the GET /api/values
which will return the blobs content.
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Mvc;
using Azure.Identity;
using Azure.Storage.Blobs;
namespace app.Controllers
{
[Route("api/[controller]")
[ApiController]
public class ValuesController : ControllerBase
{
private static Uri BlobUri = new Uri("https://tfazrolesstorageaccount.blob.core.windows.net/tf-az-roles-container/hello.txt");
// GET api/values
[HttpGet]
public async Task<string> Get(CancellationToken cancellationToken)
{
var credential = new DefaultAzureCredential();
var blob = new BlobClient(BlobUri, credential);
var content = (await blob.DownloadAsync(cancellationToken)).Value.Content;
using (var reader = new StreamReader(content))
{
var contents = await reader.ReadToEndAsync();
return contents;
}
}
}
}
We’ll publish our webapp and use the az webapp
from the Azure CLI to deploy our zipped published files.
$ dotnet publish -c Release -o ~/Desktop/publish
$ zip -j -r archive.zip ~/Desktop/publish/*
$ az webapp deployment source config-zip -g tf-az-roles-rg -n tf-az-roles-app-service --src archive.zip
To test this out, head to <your-web-name>.azurewebsites.net/api/values
and you should see the text of our uploaded file.
You can grab the code I’ve used here from my BlogCodeSamples GitHub Repo