Implement Secure Azure Solutions
This guide covers the AZ-204 exam topics for implementing secure Azure solutions:
- Secure app configuration data by using App Configuration or Azure Key Vault
- Develop code that uses keys, secrets, and certificates stored in Azure Key Vault
- Implement Managed Identities for Azure resources
We'll enhance our TaskManagerWeb
(WebApp) and TaskManagerFunctions
(Function App) to demonstrate these security practices.
Prerequisites
- .NET SDK 8.0 (Install via winget:
winget install Microsoft.DotNet.SDK.8
) - Azure CLI (Install)
- Azure Functions Core Tools (Install)
- Azure Subscription (WGU Student Azure account with resource group
az204exam
) - Microsoft 365 Developer Program Subscription (for Microsoft Entra ID access)
- Existing Apps:
- WebApp:
taskmanagerweb-yourname
(ASP.NET Core MVC, Free F1 tier) - Function App:
taskmanagerfunc-yourname
(.NET 8 isolated, Consumption plan) - Cosmos DB:
taskmanagercosmos
withTasksDB/Tasks
- Storage Account:
taskmanagerstorage
withtasks
container
Secure App Configuration Data Using Azure Key Vault
We'll store sensitive configuration data (e.g., Cosmos DB connection string) in Azure Key Vault and access it in our WebApp, ensuring secrets aren't hardcoded.
Steps
Follow these steps to secure app configuration data:
-
Create an Azure Key Vault.
-
Add the Cosmos DB connection string as a secret in Key Vault.
-
Configure the WebApp to access Key Vault.
-
Update the WebApp to use the secret.
-
Test locally (optional).
-
Deploy and test in Azure.
Step 1: Create an Azure Key Vault
In the Azure Portal (WGU Azure tenant), go to Create a resource > Key Vault > Create. Configure:
- Resource group:
az204exam
. - Key vault name:
taskmanagerkv-yourname
(must be unique). - Region: Same as your apps (e.g., West US).
- Pricing tier: Standard.
Click Create.
Step 2: Add the Cosmos DB Connection String as a Secret
In your Key Vault (taskmanagerkv-yourname
):
- Go to Secrets > Generate/Import.
- Name:
CosmosDBConnection
. - Value:
[your-cosmos-connection-string]
(copy from Cosmos DB > Keys > Primary Connection String). - Click Create.
Alternatively, use the Azure CLI:
az keyvault secret set --vault-name taskmanagerkv-yourname --name CosmosDBConnection --value "[your-cosmos-connection-string]"
Step 3: Configure the WebApp to Access Key Vault
Enable Managed Identity for the WebApp (already done in webapp.md
, but we'll verify):
- In the Azure Portal, go to
taskmanagerweb-yourname
> Identity > System assigned. - Ensure Status is On. If not, turn it on and save.
Grant the WebApp access to Key Vault:
- In
taskmanagerkv-yourname
> Access policies > Create. - Permissions: Select Get and List under Secret permissions.
- Principal: Search for and select
taskmanagerweb-yourname
. - Click Next > Create.
Step 4: Update the WebApp to Use the Secret
Add the Azure Key Vault SDK to TaskManagerWeb
:
cd TaskManagerWeb
dotnet add package Azure.Identity
dotnet add package Azure.Security.KeyVault.Secrets
dotnet add package Microsoft.Extensions.Configuration.AzureKeyVault
Update Program.cs
to load secrets from Key Vault:
using Microsoft.Identity.Web;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Configuration;
var builder = WebApplication.CreateBuilder(args);
// Add Key Vault configuration
var keyVaultName = "taskmanagerkv-yourname"; // Replace with your Key Vault name
var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/";
builder.Configuration.AddAzureKeyVault(
new Uri(keyVaultUri),
new DefaultAzureCredential());
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
// Add Microsoft Identity authentication
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization();
// Add Key Vault client as a service
builder.Services.AddSingleton(x =>
{
var client = new SecretClient(new Uri(keyVaultUri), new DefaultAzureCredential());
return client;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
Update appsettings.json
to remove the hardcoded Cosmos DB connection string (we'll fetch it from Key Vault):
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "yourtenant.onmicrosoft.com",
"TenantId": "common",
"ClientId": "your-client-id",
"CallbackPath": "/signin-oidc"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Create a service to access Cosmos DB using the Key Vault secret. Create Services/CosmosDbService.cs
:
using Microsoft.Azure.Cosmos;
using TaskManagerWeb.Models;
using Azure.Security.KeyVault.Secrets;
namespace TaskManagerWeb.Services
{
public class CosmosDbService
{
private readonly CosmosClient _cosmosClient;
private readonly Container _container;
public CosmosDbService(SecretClient secretClient)
{
var connectionStringResponse = secretClient.GetSecret("CosmosDBConnection");
var connectionString = connectionStringResponse.Value.Value;
_cosmosClient = new CosmosClient(connectionString, new CosmosClientOptions { ConnectionMode = ConnectionMode.Direct });
_container = _cosmosClient.GetContainer("TasksDB", "Tasks");
}
public async Task<List<TaskItem>> GetTasksAsync()
{
var query = new QueryDefinition("SELECT * FROM c");
var iterator = _container.GetItemQueryIterator<TaskItem>(query);
var tasks = new List<TaskItem>();
while (iterator.HasMoreResults)
{
var response = await iterator.ReadNextAsync();
tasks.AddRange(response);
}
return tasks;
}
public async Task AddTaskAsync(TaskItem task)
{
task.id = Guid.NewGuid().ToString();
await _container.CreateItemAsync(task, new PartitionKey(task.id));
}
}
}
Update Models/TaskItem.cs
to include an id
property (if not already present):
namespace TaskManagerWeb.Models
{
public class TaskItem
{
public string id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string DueDate { get; set; }
}
}
Update Controllers/HomeController.cs
to use the CosmosDbService
and persist tasks:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TaskManagerWeb.Models;
using TaskManagerWeb.Services;
using System.Collections.Generic;
using Microsoft.Graph;
namespace TaskManagerWeb.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly GraphServiceClient _graphClient;
private readonly CosmosDbService _cosmosDbService;
public HomeController(GraphServiceClient graphClient, CosmosDbService cosmosDbService)
{
_graphClient = graphClient;
_cosmosDbService = cosmosDbService;
}
public async Task<IActionResult> Index()
{
// Fetch user profile from Microsoft Graph
var user = await _graphClient.Me.Request().GetAsync();
ViewBag.UserProfile = new { DisplayName = user.DisplayName, Email = user.Mail ?? user.UserPrincipalName };
// Fetch tasks from Cosmos DB
var tasks = await _cosmosDbService.GetTasksAsync();
return View(tasks);
}
[HttpPost]
public async Task<IActionResult> Create(string title, string description, string dueDate)
{
var task = new TaskItem
{
Title = title,
Description = description,
DueDate = dueDate
};
await _cosmosDbService.AddTaskAsync(task);
return RedirectToAction("Index");
}
public IActionResult SignOut()
{
return SignOut("Cookies", OpenIdConnectDefaults.AuthenticationScheme);
}
}
}
Step 5: Test Locally (Optional)
To test locally, you'll need to authenticate with Azure to access Key Vault. Ensure you’re logged into the Azure CLI with your WGU Azure tenant credentials:
az login
Run the WebApp locally:
cd TaskManagerWeb
dotnet run
Sign in with a Microsoft 365 Developer tenant user at https://localhost:5001
. Verify that tasks are saved to Cosmos DB (TasksDB/Tasks
) and retrieved correctly. The Cosmos DB connection string should be securely fetched from Key Vault.
Step 6: Deploy and Test
Deploy to Azure (in your WGU Azure tenant):
az webapp up --name taskmanagerweb-yourname --resource-group az204exam
Test at https://taskmanagerweb-yourname.azurewebsites.net
. Sign in with a Microsoft 365 Developer tenant user. Verify that tasks are saved to Cosmos DB and the app functions as expected.
Why
Using Azure Key Vault to store sensitive configuration data like connection strings prevents hardcoding secrets in the app, enhancing security and meeting exam requirements.
Develop Code that Uses Keys, Secrets, and Certificates Stored in Azure Key Vault
We'll add a Function to TaskManagerFunctions
that uses a secret from Key Vault to encrypt task data before storing it in Cosmos DB.
Steps
Follow these steps to use secrets in Azure Key Vault:
-
Store an encryption key in Key Vault.
-
Grant the Function App access to Key Vault.
-
Update the Function to use the encryption key.
-
Test locally (optional).
-
Deploy and test in Azure.
Step 1: Store an Encryption Key in Key Vault
Generate a simple encryption key (for demo purposes, we'll use a string; in production, use a proper key):
- In
taskmanagerkv-yourname
> Secrets > Generate/Import. - Name:
EncryptionKey
. - Value:
MySuperSecretKey1234567890abcdef
(32 characters for AES-256 demo). - Click Create.
Alternatively, use the Azure CLI:
az keyvault secret set --vault-name taskmanagerkv-yourname --name EncryptionKey --value "MySuperSecretKey1234567890abcdef"
Step 2: Grant the Function App Access to Key Vault
Enable Managed Identity for the Function App:
- In the Azure Portal, go to
taskmanagerfunc-yourname
> Identity > System assigned. - Turn Status to On > Save.
Grant access to Key Vault:
- In
taskmanagerkv-yourname
> Access policies > Create. - Permissions: Select Get and List under Secret permissions.
- Principal: Search for and select
taskmanagerfunc-yourname
. - Click Next > Create.
Step 3: Update the Function to Use the Encryption Key
Add the necessary packages to TaskManagerFunctions
:
cd TaskManagerFunctions
dotnet add package Azure.Identity
dotnet add package Azure.Security.KeyVault.Secrets
dotnet add package System.Security.Cryptography.Algorithms
Update ManageTask.cs
to encrypt the task's Description
field before saving to Cosmos DB:
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Azure.Cosmos;
using Azure.Security.KeyVault.Secrets;
using Azure.Identity;
using System.Security.Cryptography;
using System.Text;
namespace TaskManagerFunctions
{
public class ManageTask
{
private readonly ILogger<ManageTask> _logger;
private readonly CosmosClient _cosmosClient;
private readonly SecretClient _secretClient;
private readonly string _encryptionKey;
public ManageTask(ILogger<ManageTask> logger)
{
_logger = logger;
_cosmosClient = new CosmosClient(Environment.GetEnvironmentVariable("CosmosDBConnection"), new CosmosClientOptions { ConnectionMode = ConnectionMode.Direct });
var keyVaultName = "taskmanagerkv-yourname"; // Replace with your Key Vault name
var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/";
_secretClient = new SecretClient(new Uri(keyVaultUri), new DefaultAzureCredential());
var secret = _secretClient.GetSecret("EncryptionKey").Value;
_encryptionKey = secret.Value;
}
public class TaskItem
{
public string id { get; set; } = Guid.NewGuid().ToString();
public string Title { get; set; }
public string Description { get; set; } // Will be encrypted
public string DueDate { get; set; }
}
[Function("ManageTask")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", "get")] HttpRequest req)
{
_logger.LogInformation("Processing task in Cosmos DB.");
var container = _cosmosClient.GetContainer("TasksDB", "Tasks");
if (req.Method == "POST")
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
TaskItem task = JsonConvert.DeserializeObject<TaskItem>(requestBody);
if (string.IsNullOrEmpty(task?.Title))
{
return new BadRequestObjectResult("Please provide a title.");
}
// Encrypt the description
if (!string.IsNullOrEmpty(task.Description))
{
task.Description = EncryptString(task.Description, _encryptionKey);
}
task.id = Guid.NewGuid().ToString();
await container.CreateItemAsync(task, new PartitionKey(task.id));
_logger.LogInformation($"Created task: {task.Title}");
return new OkObjectResult(task);
}
else // GET
{
string taskId = req.Query["taskId"];
if (string.IsNullOrEmpty(taskId))
{
return new BadRequestObjectResult("Please provide taskId.");
}
try
{
var task = await container.ReadItemAsync<TaskItem>(taskId, new PartitionKey(taskId));
// Decrypt the description
if (!string.IsNullOrEmpty(task.Resource.Description))
{
task.Resource.Description = DecryptString(task.Resource.Description, _encryptionKey);
}
_logger.LogInformation($"Read task: {task.Resource.Title}");
return new OkObjectResult(task.Resource);
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return new NotFoundResult();
}
}
}
private string EncryptString(string plainText, string key)
{
using (Aes aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32)); // Ensure 32 bytes for AES-256
aes.GenerateIV();
var iv = aes.IV;
using (var encryptor = aes.CreateEncryptor(aes.Key, iv))
using (var ms = new MemoryStream())
{
ms.Write(iv, 0, iv.Length); // Prepend IV to the ciphertext
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
using (var sw = new StreamWriter(cs))
{
sw.Write(plainText);
}
return Convert.ToBase64String(ms.ToArray());
}
}
}
private string DecryptString(string cipherText, string key)
{
var fullCipher = Convert.FromBase64String(cipherText);
using (Aes aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32));
var iv = new byte[16];
Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length); // Extract IV from the beginning
aes.IV = iv;
using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
using (var ms = new MemoryStream(fullCipher, iv.Length, fullCipher.Length - iv.Length))
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (var sr = new StreamReader(cs))
{
return sr.ReadToEnd();
}
}
}
}
}
Step 4: Test Locally (Optional)
Ensure local.settings.json
has the CosmosDBConnection
:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"CosmosDBConnection": "[your-cosmos-connection-string]"
}
}
Log in to the Azure CLI with your WGU Azure tenant credentials:
az login
Run the Function locally:
func start
POST a task:
curl -X POST http://localhost:7071/api/ManageTask -H "Content-Type: application/json" -d '{"Title":"Secure Task","Description":"Sensitive data","DueDate":"2025-04-15"}'
GET the task (use id
from POST response):
curl http://localhost:7071/api/ManageTask?taskId=[task-id]
Verify in Cosmos DB (TasksDB/Tasks
) that the Description
is encrypted. The GET response should show the decrypted description.
Step 5: Deploy and Test
Deploy the Function to Azure:
func azure functionapp publish taskmanagerfunc-yourname
Test:
curl -X POST https://taskmanagerfunc-yourname.azurewebsites.net/api/managetask?code=[your-key] -H "Content-Type: application/json" -d '{"Title":"Azure Secure Task","Description":"Sensitive data","DueDate":"2025-04-15"}'
curl https://taskmanagerfunc-yourname.azurewebsites.net/api/managetask?code=[your-key]&taskId=[task-id]
Verify the encryption/decryption process works as expected.
Why
Using Azure Key Vault to store encryption keys ensures secure data handling, a key exam skill, and demonstrates proper secret management.
Implement Managed Identities for Azure Resources
We’ve already enabled Managed Identities for both the WebApp and Function App to access Key Vault. Let’s verify and use it in the Function App to access Cosmos DB without a connection string in the configuration.
Steps
Follow these steps to implement Managed Identities:
-
Grant the Function App access to Cosmos DB using Managed Identity.
-
Update the Function to use Managed Identity for Cosmos DB.
-
Test locally (optional).
-
Deploy and test in Azure.
Step 1: Grant the Function App Access to Cosmos DB
Ensure Managed Identity is enabled for taskmanagerfunc-yourname
(already done in the previous section).
Grant the Managed Identity access to Cosmos DB:
- In the Azure Portal, go to
taskmanagercosmos
> Access control (IAM) > Add role assignment. - Role: Cosmos DB Built-in Data Contributor.
- Assign access to: Managed identity > Select
taskmanagerfunc-yourname
. - Click Save.
Step 2: Update the Function to Use Managed Identity
Update ManageTask.cs
to use Managed Identity for Cosmos DB access, removing the need for a connection string:
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Azure.Cosmos;
using Azure.Security.KeyVault.Secrets;
using Azure.Identity;
using System.Security.Cryptography;
using System.Text;
using Azure.Core;
namespace TaskManagerFunctions
{
public class ManageTask
{
private readonly ILogger<ManageTask> _logger;
private readonly CosmosClient _cosmosClient;
private readonly SecretClient _secretClient;
private readonly string _encryptionKey;
public ManageTask(ILogger<ManageTask> logger)
{
_logger = logger;
// Use Managed Identity to authenticate with Cosmos DB
var credential = new DefaultAzureCredential();
var cosmosEndpoint = "https://taskmanagercosmos.documents.azure.com:443/"; // Replace with your Cosmos DB endpoint
_cosmosClient = new CosmosClient(cosmosEndpoint, credential, new CosmosClientOptions { ConnectionMode = ConnectionMode.Direct });
// Fetch encryption key from Key Vault
var keyVaultName = "taskmanagerkv-yourname"; // Replace with your Key Vault name
var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/";
_secretClient = new SecretClient(new Uri(keyVaultUri), credential);
var secret = _secretClient.GetSecret("EncryptionKey").Value;
_encryptionKey = secret.Value;
}
public class TaskItem
{
public string id { get; set; } = Guid.NewGuid().ToString();
public string Title { get; set; }
public string Description { get; set; } // Will be encrypted
public string DueDate { get; set; }
}
[Function("ManageTask")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", "get")] HttpRequest req)
{
_logger.LogInformation("Processing task in Cosmos DB.");
var container = _cosmosClient.GetContainer("TasksDB", "Tasks");
if (req.Method == "POST")
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
TaskItem task = JsonConvert.DeserializeObject<TaskItem>(requestBody);
if (string.IsNullOrEmpty(task?.Title))
{
return new BadRequestObjectResult("Please provide a title.");
}
// Encrypt the description
if (!string.IsNullOrEmpty(task.Description))
{
task.Description = EncryptString(task.Description, _encryptionKey);
}
task.id = Guid.NewGuid().ToString();
await container.CreateItemAsync(task, new PartitionKey(task.id));
_logger.LogInformation($"Created task: {task.Title}");
return new OkObjectResult(task);
}
else // GET
{
string taskId = req.Query["taskId"];
if (string.IsNullOrEmpty(taskId))
{
return new BadRequestObjectResult("Please provide taskId.");
}
try
{
var task = await container.ReadItemAsync<TaskItem>(taskId, new PartitionKey(taskId));
// Decrypt the description
if (!string.IsNullOrEmpty(task.Resource.Description))
{
task.Resource.Description = DecryptString(task.Resource.Description, _encryptionKey);
}
_logger.LogInformation($"Read task: {task.Resource.Title}");
return new OkObjectResult(task.Resource);
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return new NotFoundResult();
}
}
}
private string EncryptString(string plainText, string key)
{
using (Aes aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32)); // Ensure 32 bytes for AES-256
aes.GenerateIV();
var iv = aes.IV;
using (var encryptor = aes.CreateEncryptor(aes.Key, iv))
using (var ms = new MemoryStream())
{
ms.Write(iv, 0, iv.Length); // Prepend IV to the ciphertext
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
using (var sw = new StreamWriter(cs))
{
sw.Write(plainText);
}
return Convert.ToBase64String(ms.ToArray());
}
}
}
private string DecryptString(string cipherText, string key)
{
var fullCipher = Convert.FromBase64String(cipherText);
using (Aes aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32));
var iv = new byte[16];
Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length); // Extract IV from the beginning
aes.IV = iv;
using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
using (var ms = new MemoryStream(fullCipher, iv.Length, fullCipher.Length - iv.Length))
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (var sr = new StreamReader(cs))
{
return sr.ReadToEnd();
}
}
}
}
}
Step 3: Test Locally (Optional)
Remove the CosmosDBConnection
from local.settings.json
since we're using Managed Identity:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
}
}
Log in to the Azure CLI with your WGU Azure tenant credentials:
az login
Run the Function locally:
func start
POST a task:
curl -X POST http://localhost:7071/api/ManageTask -H "Content-Type: application/json" -d '{"Title":"Managed Identity Task","Description":"Sensitive data","DueDate":"2025-04-15"}'
GET the task (use id
from POST response):
curl http://localhost:7071/api/ManageTask?taskId=[task-id]
Verify the task is saved and retrieved from Cosmos DB using Managed Identity.
Step 4: Deploy and Test
Deploy the Function to Azure:
func azure functionapp publish taskmanagerfunc-yourname
Test:
curl -X POST https://taskmanagerfunc-yourname.azurewebsites.net/api/managetask?code=[your-key] -H "Content-Type: application/json" -d '{"Title":"Azure Managed Identity Task","Description":"Sensitive data","DueDate":"2025-04-15"}'
curl https://taskmanagerfunc-yourname.azurewebsites.net/api/managetask?code=[your-key]&taskId=[task-id]
Verify the task is saved and retrieved using Managed Identity.
Why
Managed Identities eliminate the need to manage credentials, enhancing security and simplifying access to Azure resources, a key exam objective.
Clean Up (Optional)
To avoid costs, delete all resources:
az group delete -n az204exam --no-wait --yes
Next Steps
- Explore additional Key Vault features, such as using keys for encryption or certificates for authentication.
- Review all guides for comprehensive AZ-204 exam preparation.