Blob Storage¶
This guide explains how to enable and integrate Azure Blob Storage with API services using the Forge platform, covering both Terraform infrastructure setup and Aspire local development.
đ¯ Overview¶
Azure Blob Storage provides scalable, secure object storage for unstructured data. The Forge platform provides:
- Terraform Module: Automated Azure Blob Storage Account provisioning
- Aspire Integration: Local development and testing with storage emulator
- RBAC Authentication: Secure, token-based access via the app's Entra identity (no keys or SAS tokens)
- Feature Flags: Flexible enable/disable configuration
đ§ Infrastructure Setup (Terraform)¶
Enable Blob Storage Feature Flag¶
Blob storage is a feature flag on the saif-apiservice module, so it is configured in the module call
that the templates generate for you â infra/api/app.generated.tf.
âšī¸ Projects scaffolded before Forge 3.x keep this module call in
infra/app/app.generated.tfâ see the Forge v2 â v3 migration guide.
Add a feature_flags block to the existing module "saif-appservices" call:
feature_flags = {
enable_blob_storage = true
blob_storage_settings = {
containers = ["blobcontainer"]
contributor_group_id = "group-object-id-guid"
}
}
â ī¸
enable_blob_storageandblob_storage_settingsare members of thefeature_flagsobject â they are not top-level module arguments.
Configuration Options:
| Property | Type | Description | Example |
|---|---|---|---|
enable_blob_storage |
bool | Enable/disable blob storage deployment | true |
containers |
list(string) | List of blob container names to create | ["uploads", "archives"] |
contributor_group_id |
string | Entra ID group object ID granted contributor access to blob data ("" grants the app's managed identity only) |
"12345678-1234-1234-1234-123456789abc" |
âšī¸ Network isolation is always on and not configurable here (#1003) â the app reaches blobs over a private endpoint, and the public data-plane endpoint denies everything except SAIF's corporate/colocation IP ranges and Microsoft Defender for Storage's scanner. This module is intentionally opinionated; see Network Isolation below. If you need the pre-#1003 shape (public endpoint, no PE, RBAC-only) or otherwise need to control
private_endpoint_enabled/network_rules_enabled, use thesaif-resources/modules/storagemodule directly instead of this API service'sblob_storage_settings.â ī¸ Hardcode the object ID â don't resolve it with a
datasource (e.g.data.azuread_group.example.object_id). Terraform plans that look up the group at plan time fail the Organization Policy Check. Look up your team's Entra ID group object ID once (Entra admin center oraz ad group show --group "<display name>" --query id -o tsv) and paste the literal GUID intoblob_storage_settings.
Example Terraform Configuration¶
The generated module call already supplies owner, project_id, environment, and tags from
vars.yml â you only add the feature_flags block:
module "saif-appservices" {
source = "app.terraform.io/SAIFCorp/saif-apiservice/azure"
version = ">= 3.0.0, < 4.0.0"
# ... existing generated module arguments ...
feature_flags = {
enable_blob_storage = true
blob_storage_settings = {
containers = [
"documents",
"uploads",
"archives"
]
# Hardcoded object ID for your team's Entra ID security group â this can be a
# cloud-native Entra ID group or an on-prem AD group synced via Entra Connect/Cloud
# Sync, both resolve the same way. Do not replace this with a `data` source lookup;
# the Organization Policy Check disallows it.
contributor_group_id = "12345678-1234-1234-1234-123456789abc"
}
}
}
Generated Resources¶
When enable_blob_storage = true, the following resources are automatically created:
- đī¸ Azure Storage Account â named
{projectid}{environment_short_name}{tenant_short_name}with dashes removed and truncated to 24 characters (e.g.myapitestcorp,myapiprodcorp) - đĻ Blob Containers (with names from
containerslist, all private access) - đ RBAC Role Assignments:
- Storage Blob Data Contributor: assigned to the application's Entra app registration service principal (the runtime data-plane identity)
- Storage Blob Data Contributor: assigned to the group in
contributor_group_id - âī¸ App Setting:
ConnectionStrings__blobstorageis added to the web app with the primary blob endpoint of the new storage account - đ Private Endpoint â blob sub-resource in the services subnet, registered in the
privatelink.blob.core.windows.netDNS zone
đĄī¸ Network Isolation¶
A deny-by-default firewall is always applied to the storage account's public data-plane endpoint
(default_action = Deny, bypass = ["AzureServices"]), on top of the private endpoint â neither
is configurable from blob_storage_settings. Traffic over the private endpoint always bypasses
the firewall, so your app's access is unaffected either way.
SAIF's corporate and colocation IP ranges are always allowed through the firewall, matching the
Cosmos DB module's saif_ip_ranges pattern, so browsing from the office or a jump box on the
corporate network keeps working. Microsoft Defender for Storage's malware-scanning service is
also granted access via a private_link_access resource instance rule â the generic AzureServices
bypass does not cover it.
âšī¸
bypass = ["AzureServices"]also lets other Azure trusted services reach the account, not just the corporate ranges and Defender's scanner â see Microsoft Learn's list of trusted services for the full exception scope.
VPN Access Does Not Work
Connecting over VPN from home does not resolve to a SAIF corporate IP address, so the
Azure Portal Storage Browser and az storage CLI will get a 403 from the firewall. You must
access the storage account's data plane from a virtual machine inside the corporate network or
be on the corporate network itself â the same constraint as Cosmos DB's Data Explorer (see
Accessing Cosmos DB Data).
If your team needs off-network data-plane access beyond what the built-in corporate IP ranges
cover (e.g. a VPN egress range not already listed), or needs to opt out of network isolation
entirely (the pre-#1003 shape), that requires using the saif-resources/modules/storage module
directly â none of this is configurable from blob_storage_settings.
đĨī¸ Local Development (Aspire)¶
Aspire Storage Emulator Setup¶
For local development, use the Azure Storage Emulator integrated with Aspire:
AppHost Configuration¶
// AppHost.cs in YourApp.AppHost
var builder = DistributedApplication.CreateBuilder(args);
// Add Azure Storage with emulator
var blobs = builder
.AddAzureStorage("storage")
.RunAsEmulator()
.AddBlobs("blobstorage");
// Reference storage in your backend service
var backend = builder
.AddApi()
.WithReference(blobs);
// Add web frontend
var frontend = builder
.AddWebFrontEnd(backend);
builder.Build().Run();
Configuration Options:
.RunAsEmulator()- Uses local storage emulator (no Azure account required).AddBlobs("blobstorage")- Adds blob storage resource with specified name (used to reference in services).WithReference()- Injects storage connection details into service
Using Blob Storage in Your Service¶
Service Registration¶
In your Program.cs, register the Azure Blob Service client:
â ī¸ The connection name passed to
AddAzureBlobServiceClientmust match the name given toAddBlobsin the AppHost, and theConnectionStrings__{name}app setting Forge creates in Azure â otherwise the client cannot resolve a connection string at startup.âšī¸
AddAzureBlobServiceClientautomatically usesDefaultAzureCredentialwhen deployed to Azure and connection strings for local development. In Azure, the platform pins the credential chain to the app registration service principal (AZURE_TOKEN_CREDENTIALS=environmentcredential), which holds the blob RBAC role.
Minimal API Examples¶
using Azure.Storage.Blobs;
// Upload endpoint
app.MapPost("/api/upload", async (IFormFile file, BlobServiceClient blobServiceClient) =>
{
if (file is null || file.Length == 0)
{
return Results.BadRequest(new { Error = "No file provided" });
}
var containerClient = blobServiceClient.GetBlobContainerClient("uploads");
await containerClient.CreateIfNotExistsAsync();
var blobName = $"{Guid.NewGuid()}-{file.FileName}";
var blobClient = containerClient.GetBlobClient(blobName);
await using var stream = file.OpenReadStream();
await blobClient.UploadAsync(stream, overwrite: true);
return Results.Ok(new
{
FileName = file.FileName,
BlobName = blobName,
Size = file.Length,
UploadedAt = DateTime.UtcNow
});
});
// List files endpoint
app.MapGet("/api/files", async (BlobServiceClient blobServiceClient) =>
{
var containerClient = blobServiceClient.GetBlobContainerClient("uploads");
if (!await containerClient.ExistsAsync())
{
return Results.Ok(Array.Empty<object>());
}
var blobs = new List<object>();
await foreach (var blob in containerClient.GetBlobsAsync())
{
blobs.Add(new
{
Name = blob.Name,
Size = blob.Properties.ContentLength,
CreatedOn = blob.Properties.CreatedOn
});
}
return Results.Ok(blobs);
});
đ Example: Aspire Blob Storage Project¶
The Forge platform includes a complete example at foundry/dotnet/aspire-blobstorage/:
Project Structure¶
aspire-blobstorage/
âââ infra/
â âââ api/ # API service Terraform (app.generated.tf lives here)
â âââ auth/ # Entra ID (corp) and Okta (ext) auth configuration
â âââ web/ # Front-end Terraform
âââ src/
â âââ blobstorage.AppHost/ # Aspire orchestration
â â âââ AppHost.cs # Service configuration
â â âââ Extensions.cs # Aspire extensions
â âââ blobstorage/ # API backend
â â âââ Program.cs # Blob upload/list endpoints
â âââ blobstorage.Frontend/ # React frontend
â â âââ [UI components]
â âââ blobstorage.TypeSpec/ # OpenAPI definitions
âââ README.md
Running the Example¶
cd foundry/dotnet/aspire-blobstorage
# Start with Aspire orchestration
dotnet run --project src/blobstorage.AppHost
The Aspire dashboard URL is printed in the console at startup (the port comes from
src/blobstorage.AppHost/Properties/launchSettings.json).
The dashboard shows:
- đī¸ Storage Account (Emulator)
- đĻ Blob Containers
- đ Service interactions
- đ Resource metrics
đ Configuration Checklist¶
-
Infrastructure
-
Add
enable_blob_storage = trueto thefeature_flagsblock ininfra/api/app.generated.tf - Define container names in
containerslist - Obtain contributor group object ID
- Apply Terraform configuration
-
Verify storage account created in Azure
-
Application
-
Add
builder.AddAzureBlobServiceClient("blobstorage")to Program.cs - Implement blob upload/download endpoints using
BlobServiceClient -
Test with RBAC authentication
-
Local Development
-
Add Aspire storage emulator to AppHost with
.AddAzureStorage().RunAsEmulator().AddBlobs("blobstorage") - Reference storage in backend service with
.WithReference(blobs) -
Test upload/download locally
-
Testing
- Test blob upload functionality
- Test blob download functionality
- Verify RBAC permissions in Azure
- Test with Aspire emulator locally
đ Deployment¶
Monitoring & Diagnostics¶
The storage account automatically includes:
- đ Diagnostic Settings (Azure Monitor)
- đ Metrics (requests, latency, errors)
- đ Activity Logs (operations audit trail)
- đ Storage Analytics (capacity and transaction metrics)
đ Related Resources¶
- Azure Blob Storage Documentation
- Aspire Storage Integration
- Azure Identity & RBAC
- Example: aspire-blobstorage
â FAQ¶
Q: Can I use connection strings instead of RBAC?
A: Yes, but RBAC is recommended for production. Connection strings are best for local development with the storage emulator.
Q: How do I add more containers after initial deployment?
A: Update the containers list in the feature flags and reapply Terraform.
Q: What happens if a container name is invalid?
A: Terraform validation will fail with a helpful error message. Container names must be 3-63 characters, lowercase, and contain only alphanumeric characters and hyphens.
Q: Can I access storage from multiple services?
A: Yes, multiple services can reference the same storage account. Just ensure they all have appropriate RBAC roles assigned.
Q: How do I monitor blob storage usage?
A: Use Azure Portal's Storage Account monitoring, Azure Monitor dashboards, or configure custom alerts on metrics like capacity and transaction count.
Q: Why am I getting a 403 from the Azure Portal Storage Browser or az storage CLI?
A: The storage firewall is always on (#1003) and blocks access from outside SAIF's corporate/colocation IP ranges. You must access the storage data plane from a machine on the corporate network â VPN from home is not sufficient, since it doesn't egress from a SAIF corporate IP. This is the same constraint as Cosmos DB's Data Explorer; see VPN Access Does Not Work above.