O Sertão será Cloud

How to Configure Azure Blob Service Client Applying SOLID Principles in an Azure Function with…

How to Configure Azure Blob Service Client Applying SOLID Principles in an Azure Function with TypeScript

Azure Blob Storage is a Microsoft object storage solution that allows storing large amounts of unstructured data. Proper configuration of the BlobServiceClient is crucial to ensure security and efficiency. In this article, we will demonstrate how to configure an Azure Function to authenticate to Azure Blob Storage using different authentication methods (System-assigned Managed Identity, User-assigned Managed Identity, and Service Principal) applying SOLID principles.

SOLID Principles

Before we start the implementation, let’s briefly review the SOLID principles:

  1. Single Responsibility Principle (SRP): Each class should have a single responsibility.
  2. Open/Closed Principle (OCP): Classes should be open for extension but closed for modification.
  3. Liskov Substitution Principle (LSP): Subclasses should be able to replace their base classes.
  4. Interface Segregation Principle (ISP): Many specific interfaces are better than one general-purpose interface.
  5. Dependency Inversion Principle (DIP): Depend on abstractions, not on concrete implementations.

Step 1: Setting Up the Environment

Before starting, make sure you have the following dependencies installed:

npm install @azure/identity @azure/storage-blob

Step 2: Creating the CredentialProvider Abstraction

We will create a CredentialProvider interface that defines a getCredential method.

import { TokenCredential } from "@azure/identity";

/**
* Interface for providing Azure credentials.
*/
interface CredentialProvider {
/**
* Retrieves the Azure credential.
* @returns {TokenCredential} The Azure credential.
*/
getCredential(): TokenCredential;
}

Step 3: Implementing Credential Providers

We will create different implementations of CredentialProvider for various authentication methods.

Method 1: SystemAssignedManagedIdentityCredentialProvider

import { DefaultAzureCredential } from "@azure/identity";

/**
* Provides credentials for system-assigned managed identity.
*/
class SystemAssignedManagedIdentityCredentialProvider implements CredentialProvider {
/**
* Retrieves the Azure credential for system-assigned managed identity.
* @returns {TokenCredential} The Azure credential.
*/
getCredential(): TokenCredential {
return new DefaultAzureCredential();
}
}

Method 2: UserAssignedManagedIdentityCredentialProvider

import { DefaultAzureCredential } from "@azure/identity";

/**
* Provides credentials for user-assigned managed identity.
*/
class UserAssignedManagedIdentityCredentialProvider implements CredentialProvider {
private clientId: string;

/**
* Creates an instance of UserAssignedManagedIdentityCredentialProvider.
* @param {string} clientId - The client ID of the user-assigned managed identity.
*/
constructor(clientId: string) {
this.clientId = clientId;
}

/**
* Retrieves the Azure credential for user-assigned managed identity.
* @returns {TokenCredential} The Azure credential.
*/
getCredential(): TokenCredential {
return new DefaultAzureCredential({
managedIdentityClientId: this.clientId
});
}
};

Method 3: ServicePrincipalCredentialProvider

import { ClientSecretCredential } from "@azure/identity";

/**
* Provides credentials for service principal authentication.
*/
class ServicePrincipalCredentialProvider implements CredentialProvider {
private tenantId: string;
private clientId: string;
private clientSecret: string;

/**
* Creates an instance of ServicePrincipalCredentialProvider.
* @param {string} tenantId - The tenant ID.
* @param {string} clientId - The client ID.
* @param {string} clientSecret - The client secret.
*/
constructor(tenantId: string, clientId: string, clientSecret: string) {
this.tenantId = tenantId;
this.clientId = clientId;
this.clientSecret = clientSecret;
}

/**
* Retrieves the Azure credential for service principal authentication.
* @returns {TokenCredential} The Azure credential.
*/
getCredential(): TokenCredential {
return new ClientSecretCredential(this.tenantId, this.clientId, this.clientSecret);
}
}

Step 4: Creating the BlobServiceClientFactory

Now let’s create a factory that uses the CredentialProvider to create instances of BlobServiceClient.

import { BlobServiceClient } from "@azure/storage-blob";

/**
* Factory for creating instances of BlobServiceClient.
*/
class BlobServiceClientFactory {
private accountUrl: string;
private credentialProvider: CredentialProvider;

/**
* Creates an instance of BlobServiceClientFactory.
* @param {string} accountUrl - The account URL for the BlobServiceClient.
* @param {CredentialProvider} credentialProvider - The provider for Azure credentials.
*/
constructor(accountUrl: string, credentialProvider: CredentialProvider) {
this.accountUrl = accountUrl;
this.credentialProvider = credentialProvider;
}

/**
* Creates a BlobServiceClient instance.
* @returns {BlobServiceClient} The BlobServiceClient instance.
*/
createBlobServiceClient(): BlobServiceClient {
const credential = this.credentialProvider.getCredential();
return new BlobServiceClient(this.accountUrl, credential);
}
}

Step 5: Using the Factory in an Azure Function

Let’s see how we can use the factory to create an instance of BlobServiceClient within an Azure Function. We will demonstrate three examples: System-assigned Managed Identity, User-assigned Managed Identity, and Service Principal.

Example 1: Azure Function with System-assigned Managed Identity

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { BlobServiceClientFactory } from "./BlobServiceClientFactory";
import { SystemAssignedManagedIdentityCredentialProvider } from "./SystemAssignedManagedIdentityCredentialProvider";

/**
* Azure Function triggered by HTTP request.
* Retrieves an image from Azure Blob Storage using system-assigned managed identity.
* @param context The Azure Function context.
* @param req The HTTP request.
*/
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
const accountUrl = process.env.AZURE_STORAGEBLOB_RESOURCEENDPOINT;
const credentialProvider = new SystemAssignedManagedIdentityCredentialProvider();
const blobServiceClientFactory = new BlobServiceClientFactory(accountUrl, credentialProvider);

const blobServiceClient = blobServiceClientFactory.createBlobServiceClient();
const containerClient = blobServiceClient.getContainerClient("my-container");
const blockBlobClient = containerClient.getBlockBlobClient("my-image.jpg");
const downloadBlockBlobResponse = await blockBlobClient.download(0);
const imageBuffer = await streamToBuffer(downloadBlockBlobResponse.readableStreamBody);

context.res = {
status: 200,
headers: {
'Content-Type': 'image/jpeg'
},
body: imageBuffer
};
};

/**
* Converts a readable stream into a buffer.
* @param readableStream The readable stream to convert.
* @returns A Promise resolving to the buffer containing stream data.
*/
async function streamToBuffer(readableStream): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const chunks = [];
readableStream.on("data", (data) => {
chunks.push(data instanceof Buffer ? data : Buffer.from(data));
});
readableStream.on("end", () => {
resolve(Buffer.concat(chunks));
});
readableStream.on("error", reject);
});
}

export default httpTrigger;

Example 2: Azure Function with User-assigned Managed Identity

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { BlobServiceClientFactory } from "./BlobServiceClientFactory";
import { UserAssignedManagedIdentityCredentialProvider } from "./UserAssignedManagedIdentityCredentialProvider";

/**
* Azure Function triggered by HTTP request.
* Retrieves an image from Azure Blob Storage using user-assigned managed identity.
* @param context The Azure Function context.
* @param req The HTTP request.
*/
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
const accountUrl = process.env.AZURE_STORAGEBLOB_RESOURCEENDPOINT;
const clientId = process.env.AZURE_STORAGEBLOB_CLIENTID;
const credentialProvider = new UserAssignedManagedIdentityCredentialProvider(clientId);
const blobServiceClientFactory = new BlobServiceClientFactory(accountUrl, credentialProvider);

const blobServiceClient = blobServiceClientFactory.createBlobServiceClient();
const containerClient = blobServiceClient.getContainerClient("my-container");
const blockBlobClient = containerClient.getBlockBlobClient("my-image.jpg");
const downloadBlockBlobResponse = await blockBlobClient.download(0);
const imageBuffer = await streamToBuffer(downloadBlockBlobResponse.readableStreamBody);

context.res = {
status: 200,
headers: {
'Content-Type': 'image/jpeg'
},
body: imageBuffer
};
};

/**
* Converts a readable stream into a buffer.
* @param readableStream The readable stream to convert.
* @returns A Promise resolving to the buffer containing stream data.
*/
async function streamToBuffer(readableStream): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const chunks = [];
readableStream.on("data", (data) => {
chunks.push(data instanceof Buffer ? data : Buffer.from(data));
});
readableStream.on("end", () => {
resolve(Buffer.concat(chunks));
});
readableStream.on("error", reject);
});
}

export default httpTrigger;

Example 3: Azure Function with Service Principal

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { BlobServiceClientFactory } from "./BlobServiceClientFactory";
import { ServicePrincipalCredentialProvider } from "./ServicePrincipalCredentialProvider";

/**
* Azure Function triggered by HTTP request.
* Retrieves an image from Azure Blob Storage using service principal authentication.
* @param context The Azure Function context.
* @param req The HTTP request.
*/
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
const accountUrl = process.env.AZURE_STORAGEBLOB_RESOURCEENDPOINT;
const tenantId = process.env.AZURE_STORAGEBLOB_TENANTID;
const clientId = process.env.AZURE_STORAGEBLOB_CLIENTID;
const clientSecret = process.env.AZURE_STORAGEBLOB_CLIENTSECRET;
const credentialProvider = new ServicePrincipalCredentialProvider(tenantId, clientId, clientSecret);
const blobServiceClientFactory = new BlobServiceClientFactory(accountUrl, credentialProvider);

const blobServiceClient = blobServiceClientFactory.createBlobServiceClient();
const containerClient = blobServiceClient.getContainerClient("my-container");
const blockBlobClient = containerClient.getBlockBlobClient("my-image.jpg");
const downloadBlockBlobResponse = await blockBlobClient.download(0);
const imageBuffer = await streamToBuffer(downloadBlockBlobResponse.readableStreamBody);

context.res = {
status: 200,
headers: {
'Content-Type': 'image/jpeg'
},
body: imageBuffer
};
};

/**
* Converts a readable stream into a buffer.
* @param readableStream The readable stream to convert.
* @returns A Promise resolving to the buffer containing stream data.
*/
async function streamToBuffer(readableStream): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const chunks = [];
readableStream.on("data", (data) => {
chunks.push(data instanceof Buffer ? data : Buffer.from(data));
});
readableStream.on("end", () => {
resolve(Buffer.concat(chunks));
});
readableStream.on("error", reject);
});
}

export default httpTrigger;

Conclusion

Applying SOLID principles, we have created a modular and extensible design for configuring the BlobServiceClient. This design facilitates the addition of new authentication methods and code maintenance. By following these steps, you can ensure that your code is more organized, easier to understand, and maintainable.

References

Início Rápido – Criar uma conexão de serviço no aplicativo de funções por meio do portal do Azure


How to Configure Azure Blob Service Client Applying SOLID Principles in an Azure Function with… was originally published in Stackademic on Medium, where people are continuing the conversation by highlighting and responding to this story.