Using Workload Idenity to Access Azure Blob Storage

In this post we'll show the steps needed to create an AKS cluster, enabled with Azure Workload Identity and then we'll build a sample dotnet app that writes files to Azure Blob Storage

Posted on: Dec 19, 2023

Steve Griffith

Steve Griffith is a Principal Cloud Architect at Microsoft, on the Digital and App Innovation, Azure Global Black Belt Team.

Workload Identity to Blob Storage

The following walkthrough shows how you can using Azure Workload Identity with the AKS Workload Identity add-on along with MSAL to access an Azure Blob Storage Account.

Cluster Creation

Now lets create the AKS cluster with the OIDC Issuer and Workload Identity add-on enabled.

RG=WorkloadIdentityRG
LOC=eastus
CLUSTER_NAME=wilab
UNIQUE_ID=$CLUSTER_NAME$RANDOM
ACR_NAME=$UNIQUE_ID
STORAGE_ACCT_NAME=griffdemo

# Create the resource group
az group create -g $RG -l $LOC

# Create the cluster with the OIDC Issuer and Workload Identity enabled
az aks create -g $RG -n $CLUSTER_NAME \
--node-count 1 \
--enable-oidc-issuer \
--enable-workload-identity \
--generate-ssh-keys

# Get the cluster credentials
az aks get-credentials -g $RG -n $CLUSTER_NAME

Set up the identity

In order to federate a managed identity with a Kubernetes Service Account we need to get the AKS OIDC Issure URL, create the Managed Identity and Service Account and then create the federation.

# Get the OIDC Issuer URL
export AKS_OIDC_ISSUER="$(az aks show -n $CLUSTER_NAME -g $RG --query "oidcIssuerProfile.issuerUrl" -otsv)"

# Create the managed identity
az identity create --name wi-demo-identity --resource-group $RG --location $LOC

# Get identity client ID
export USER_ASSIGNED_CLIENT_ID=$(az identity show --resource-group $RG --name wi-demo-identity --query 'clientId' -o tsv)

# Create a service account to federate with the managed identity
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    azure.workload.identity/client-id: ${USER_ASSIGNED_CLIENT_ID}
  labels:
    azure.workload.identity/use: "true"
  name: wi-demo-sa
  namespace: default
EOF

# Federate the identity
az identity federated-credential create \
--name wi-demo-federated-id \
--identity-name wi-demo-identity \
--resource-group $RG \
--issuer ${AKS_OIDC_ISSUER} \
--subject system:serviceaccount:default:wi-demo-sa

Create the Blob Storage Account

# Create a blob storage account
az storage account create \
--name $STORAGE_ACCT_NAME \
--resource-group $RG \
--location $LOC \
--sku Standard_LRS \
--encryption-services blob

# Get the resource ID of the storage account
STORAGE_ACCT_ID=$(az storage account show -g $RG -n $STORAGE_ACCT_NAME --query id -o tsv)

# Get the current signed in user ID
CURRENT_USER=$(az ad signed-in-user show --query id -o tsv)

# Grant the current user contributor rights for testing
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee $CURRENT_USER \
--scope "${STORAGE_ACCT_ID}"

# Grant the managed identity contributor rights
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee $USER_ASSIGNED_CLIENT_ID \
--scope "${STORAGE_ACCT_ID}"

# Create a storage account container with login auth mode enabled
az storage container create --account-name $STORAGE_ACCT_NAME --name data --auth-mode login

Create the sample app

# Create and test a new console app
dotnet new console -n blob-console-app
cd blob-console-app
dotnet run

# Add the Key Vault and Azure Identity Packages
dotnet add package Azure.Storage.Blobs
dotnet add package Azure.Identity

Edit the app as follows:

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using System;
using System.IO;
using Azure.Identity;

class Program
    {
        static void Main(string[] args)
        {
          // Get Storage Account Name
          string? storageAcctName = Environment.GetEnvironmentVariable("STORAGE_ACCT_NAME");;
          // Get the Storage Container Name
          string? containerName = Environment.GetEnvironmentVariable("CONTAINER_NAME");;

          // Check values for null or empty
          if (string.IsNullOrEmpty(storageAcctName)||string.IsNullOrEmpty(containerName))
          {
            Console.WriteLine("Storage Account or Container Name are null or empty");
            Environment.Exit(0);
          }

          while (true)
          {
            MainAsync(storageAcctName,containerName).Wait();
            System.Threading.Thread.Sleep(5000);
          }
          
        }

        static async Task MainAsync(string storageAcctName, string containerName)
        {
          var blobServiceClient = new BlobServiceClient(
                  new Uri(String.Format("https://{0}.blob.core.windows.net",storageAcctName)),
                  new DefaultAzureCredential());

          BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(containerName);

          // Create a local file in the ./data/ directory for uploading and downloading
          string localPath = "data";
          Directory.CreateDirectory(localPath);
          string fileName = Guid.NewGuid().ToString() + ".txt";
          string localFilePath = Path.Combine(localPath, fileName);

          // Write text to the file
          await File.WriteAllTextAsync(localFilePath, "Hello, World!");

          // Get a reference to a blob
          BlobClient blobClient = containerClient.GetBlobClient(fileName);

          Console.WriteLine("Uploading to Blob storage as blob:\n\t {0}\n", blobClient.Uri);

          // Upload data from the local file
          await blobClient.UploadAsync(localFilePath, true);
        }

    }

Create a new Dockerfile with the following:

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
WORKDIR /App

# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -c Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT ["dotnet", "blob-console-app.dll"]

Build the image. I’ll create an Azure Container Registry and build there, and then link that ACR to my AKS cluster.

# Create the ACR
az acr create -g $RG -n $ACR_NAME --sku Standard

# Build the image
az acr build -t wi-blob-test -r $ACR_NAME .

# Link the ACR to the AKS cluster
az aks update -g $RG -n $CLUSTER_NAME --attach-acr $ACR_NAME

Now deploy a pod that runs our blob storage app using the service account identity.


cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: wi-blob-test
  namespace: default
  labels:
    azure.workload.identity/use: "true"  
spec:
  serviceAccountName: wi-demo-sa
  containers:
    - image: ${ACR_NAME}.azurecr.io/wi-blob-test
      name: wi-blob-test
      env:
      - name: STORAGE_ACCT_NAME
        value: ${STORAGE_ACCT_NAME}
      - name: CONTAINER_NAME
        value: data      
  nodeSelector:
    kubernetes.io/os: linux
EOF

# Check the pod logs
kubectl logs -f wi-blob-test

# Sample Output
Uploading to Blob storage as blob:
	 https://griffdemo.blob.core.windows.net/data/quickstart3efa9a81-9672-4617-a6ff-f11fb93d7c84.txt

Uploading to Blob storage as blob:
	 https://griffdemo.blob.core.windows.net/data/quickstart23968d6b-80c5-4c82-8bcf-860fa00edbd3.txt

Uploading to Blob storage as blob:
	 https://griffdemo.blob.core.windows.net/data/quickstart0e20e7ef-c3ba-4fd3-a3d5-c27579d2ba96.txt

Conclusion

Congrats! You should now have a working pod that uses MSAL along with a Kubernetes Service Account federated to an Azure Managed Identity to access Azure Blob Storage.