Platform
Azure Web Application Deployment and Storage Integration Flow
A deployment-oriented walkthrough of Azure Web Apps, backend APIs, storage integration, images, environment variables, and operations checks.

When practicing Azure, if App Service, Storage Account, Blob Container, Backend API, and Frontend deployment are handled separately, it is difficult to get a sense of how the services are connected. From an actual application perspective, the user uploads a file, the front end calls the API, the back end saves the object in storage, and the saved URL or metadata remains in the database.
This article is a record of reorganizing Azure Storage and Web App into “paths where deployed web applications receive and store files” rather than a simple list of resources. The key is not how to create Blob storage, but rather which layers have what responsibilities and which settings reduce problems during operation.
Entire flow
Considering the file upload function, the configuration is relatively simple.
The front-end can upload files directly to Blob Storage, but from an operational perspective, going through the back-end API is easier to handle. This is because file size limits, extension verification, user permissions, virus scanning, metadata storage, and audit logs can be controlled through the API. When large files or cost optimization become important, the structure can be changed to one in which the backend issues a short-lived SAS URL and the client uploads it directly to the blob.
Resource groups and deployment units
In small labs, you can put all resources in one resource group. However, operationally, it is important to remember that a resource group is a unit of deletion and permissions. If web apps, storage, monitoring, and databases have the same life cycle, they can be grouped into one resource group. Conversely, it is safer to separate resources used by multiple apps, such as public networks, public monitoring, and shared Key Vault, into separate groups.
az group create \
--name rg-web-storage-prod \
--location koreacentralThe command itself is simple, but the naming convention becomes important later. Having the environment, workload, region, and resource type in the name makes it easier to track resources in the portal and logs.
rg-web-storage-prod
stwebstorageprod001
app-webstorage-api-prod
appi-webstorage-prodSettings viewed when creating a Storage Account
Storage Account provides several services such as Blob, Queue, Table, and File. Blob storage is mainly used in the file upload function.
az storage account create \
--name stwebstorageprod001 \
--resource-group rg-web-storage-prod \
--location koreacentral \
--sku Standard_LRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--allow-blob-public-access falseInitially, Standard_LRS is sufficient. However, in an operational environment, replication options such as ZRS, GRS, and RA-GRS should be reviewed depending on data importance and recovery goals. Static images, temporary upload files, contracts, settlement files, and files subject to audit cannot be placed under the same policy.
Container clearly defines the scope of access.
az storage container create \
--name uploads \
--account-name stwebstorageprod001 \
--auth-mode login \
--public-access offUnless it is a file that anyone can read, such as a public image CDN, the default value is private, which is safe. A good structure is to ensure that the blob's URL cannot be read immediately just because it is known, and to issue a download URL after checking permissions through the API if necessary.
When creating a Storage Account, there are many options, even just looking at blobs. In practice, we only check the flow of creating a container and uploading an image, but in operation, we must first determine what responsibilities the repository has.
Blob Storage
- user uploaded images
- generated reports
- exported CSV / PDF
- static assets
File Share
- lift-and-shift application shared path
- legacy app file dependency
Table Storage
- simple key-value style metadata
- low-cost lookup data
Queue Storage
- async processing request
- image resize / virus scan triggerA common mistake with file upload functionality is to treat all files in the same container and with the same lifecycle simply because they are "stored in a blob." Temporary uploads, user profile images, audited documents, and batch processing results have different access rights and retention periods. You must divide containers or separate retention policies using metadata, tags, and lifecycle policies.
az storage account management-policy create \
--account-name stwebstorageprod001 \
--resource-group rg-web-storage-prod \
--policy @lifecycle-policy.json{
"rules": [
{
"enabled": true,
"name": "delete-temp-uploads",
"type": "Lifecycle",
"definition": {
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["uploads/tmp/"]
},
"actions": {
"baseBlob": {
"delete": {
"daysAfterModificationGreaterThan": 7
}
}
}
}
}
]
}This policy helps reduce operational debris such as "temporary files left behind after failed uploads." However, because the deletion policy has an irreversible effect, it is safer to start with a narrow scope of prefix and container, and first check the actual matching target through logs and monitoring before applying it.
Think Managed Identity before Access Keys
When you create a Storage Account, an access key is created. In practice, it is easy to copy the connection string and put it in the application environment variable. However, in operation, if the key is leaked, it can lead to full storage permissions. If possible, first consider granting a Managed Identity to App Service and attaching a minimum privilege role such as Storage Blob Data Contributor.
az webapp identity assign \
--name app-webstorage-api-prod \
--resource-group rg-web-storage-prodAfter checking the principal ID of the Managed Identity, assign a role to the Storage Account scope.
PRINCIPAL_ID=$(az webapp identity show \
--name app-webstorage-api-prod \
--resource-group rg-web-storage-prod \
--query principalId \
--output tsv)
STORAGE_ID=$(az storage account show \
--name stwebstorageprod001 \
--resource-group rg-web-storage-prod \
--query id \
--output tsv)
az role assignment create \
--assignee "$PRINCIPAL_ID" \
--role "Storage Blob Data Contributor" \
--scope "$STORAGE_ID"This method helps reduce secrets. Of course, Managed Identity cannot be used immediately in all environments, so if you need to use a connection string, put it in Key Vault or App Service Configuration and do not hard-code it in the code.
Responsibilities of Backend API
The file upload API is not an endpoint that simply receives multiparts and stores them in a blob. When considering operations, we have the following responsibilities:
1. Authenticate the user and verify permissions
2. Enforce file size limits
3. Validate allowed extensions and MIME types
4. Generate the storage path
5. Upload the blob
6. Save metadata
7. Clean up after failures
8. Verify download permission
9. Record audit logsExpressed in FastAPI, the structure is similar to the following.
from fastapi import APIRouter, File, UploadFile, HTTPException
router = APIRouter()
ALLOWED_TYPES = {"image/png", "image/jpeg", "application/pdf"}
MAX_SIZE = 10 * 1024 * 1024
@router.post("/files")
async def upload_file(file: UploadFile = File(...)):
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(status_code=400, detail="Unsupported file type")
content = await file.read()
if len(content) > MAX_SIZE:
raise HTTPException(status_code=413, detail="File is too large")
blob_name = build_blob_name(file.filename)
await upload_to_blob(blob_name, content, file.content_type)
return {"blobName": blob_name}In actual implementation, streaming should be considered so as not to load the entire file into memory. As upload sizes grow, App Service timeouts, reverse proxy limitations, browser retries, and network failures must be dealt with. At this time, patterns such as direct upload with SAS, chunk upload, and background processing are reviewed.
Boundaries when using SAS URLs
SAS is a token that allows certain blob operations for a limited time. This is useful when a client needs to upload large files directly to Blob storage. However, issuing a long SAS or granting broad permissions creates a risk similar to exposing the access key.
good
- permission: write only
- scope: single blob path
- expiry: short-lived
- generated after user authorization
avoid
- permission: read/write/delete/list all
- scope: whole container
- expiry: several days or months
- generated without audit trailThe flow can be divided as follows:
In this structure, the file itself goes directly to the blob, but the API is still responsible for determining permissions and storing metadata. Therefore, it is more accurate to say “the client uses temporary upload permissions within the scope allowed by the API” rather than “the frontend uploads directly to the blob.”
App Service deployment and environment variables
App Service is a managed service that makes it easy to deploy web applications. You can choose runtimes such as Node.js, Python, Java, and .NET, and it is easy to connect with GitHub Actions.
az appservice plan create \
--name plan-webstorage-prod \
--resource-group rg-web-storage-prod \
--sku B1 \
--is-linux
az webapp create \
--name app-webstorage-api-prod \
--resource-group rg-web-storage-prod \
--plan plan-webstorage-prod \
--runtime "PYTHON:3.11"Application settings are injected as environment variables.
az webapp config appsettings set \
--name app-webstorage-api-prod \
--resource-group rg-web-storage-prod \
--settings \
STORAGE_ACCOUNT_NAME=stwebstorageprod001 \
STORAGE_CONTAINER_NAME=uploads \
APP_ENV=prodIf you need to enter the connection string or secret directly, use App Service Configuration or Key Vault reference. Uploading .env files to Git should be avoided.
local development
.env.local
cloud runtime
App Service Configuration
Key Vault reference
Managed IdentityAfter deployment, check “deployment success” and “service normality” separately.
az webapp log tail \
--name app-webstorage-api-prod \
--resource-group rg-web-storage-prod
curl -v https://app-webstorage-api-prod.azurewebsites.net/healthBuild and deployment may be successful, but health check may fail. In this case, check the runtime version, startup command, environment variables, outbound network, and storage permissions in order.
Check API and front-end deployment separately
When deploying an API and a front-end as an App Service, the responsibilities of the two services are viewed separately. The API is responsible for file verification, storage, metadata, and permission checks, and the frontend allows users to understand the upload status. In the lab, the .NET API and Web App were created separately and verified by zip distribution, but the same flow also applies to Python FastAPI, NestJS, and Spring Boot.
az webapp deployment source config-zip \
--resource-group rg-web-storage-prod \
--name app-webstorage-api-prod \
--src api.zip
az webapp deployment source config-zip \
--resource-group rg-web-storage-prod \
--name app-webstorage-web-prod \
--src web.zipImmediately after distribution, do not just check the screen with a browser. Check API, settings, storage, and logs separately.
# API process and runtime
curl -i https://app-webstorage-api-prod.azurewebsites.net/health
# App settings injected into runtime
az webapp config appsettings list \
--resource-group rg-web-storage-prod \
--name app-webstorage-api-prod \
--query "[].name" \
--output table
# Blob container access path
az storage blob list \
--account-name stwebstorageprod001 \
--container-name uploads \
--auth-mode login \
--output tableWhen testing an image upload feature, failure cases matter more than the happy path. Files that are too large, extensions that are not allowed, requests without authentication, re-uploads of the same file name, storage failures, and metadata save failures must be looked at separately. In particular, there is a difference between the case where the blob upload was successful but the DB save failed, and the case where the DB save was successful but the blob upload failed. To reduce this inconsistency, reserve the blob name first, update the metadata status after upload completion, or set up a compensation task to clean up failed temporary blobs.
upload transaction boundary
1. create upload request
2. validate user and file policy
3. reserve blob path
4. upload blob
5. persist metadata
6. publish audit event
7. mark upload as completedFor a static frontend, you can also use Azure Static Web Apps. In this case, screen deployment and API deployment can be more clearly divided, and the GitHub Actions-based deployment flow is also simplified. However, if a separate API domain exists, CORS, authentication token delivery, and API base URL management for each environment become important.
local
VITE_API_BASE_URL=http://localhost:8000
staging
VITE_API_BASE_URL=https://app-webstorage-api-stg.azurewebsites.net
production
VITE_API_BASE_URL=https://api.example.comIn the deployment pipeline, you need to check which API the frontend is looking at, whether it is fixed at build time or can be changed through runtime settings. An incorrect API base URL is not a code problem but an environment setting problem, but it appears to the user as “upload not possible.”
Frontend and CORS
If the frontend and API are in different domains, CORS configuration is required. Allow localhost:5173 in the development environment and the actual front-end domain in the production environment.
az webapp cors add \
--name app-webstorage-api-prod \
--resource-group rg-web-storage-prod \
--allowed-origins https://www.example.comCORS errors are a security policy that browsers block, so it can be confusing just by looking at the server log. The API itself returned 200, which may appear to be a failure in the browser Console. At this time, check the preflight OPTIONS request, Access-Control-Allow-Origin, Access-Control-Allow-Headers, and Access-Control-Allow-Methods in the Network tab.
When uploading files, the header tends to increase. If you use an authentication token, content-type, or custom header, preflight occurs. If the backend does not properly process the OPTIONS request, it will not proceed to the actual POST.
Browser
-> OPTIONS /files
<- CORS headers
-> POST /files
<- upload resultStorage security settings do not trust the default values.
Storage Accounts are easy to create, but leaving the default settings can result in access boundaries that are wider than intended. In particular, it is better to check public access, shared key access, TLS version, network access, and diagnostic log at the beginning.
az storage account update \
--name stwebstorageprod001 \
--resource-group rg-web-storage-prod \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--https-only trueIf possible, narrow the access boundary between App Service and Storage rather than leaving the entire public endpoint open. If you can afford the cost and complexity, look at Private Endpoint, and at least network rules and RBAC together. If you block only the network and use a wide access key, or only attach RBAC and leave the container open to the public, the boundary on one side becomes weak.
storage hardening checklist
identity
- Managed Identity is enabled for backend runtime
- RBAC role is scoped to storage account or container
- shared key usage is reviewed
network
- public blob access is disabled
- private endpoint or firewall rules are considered
- CORS origin is explicit, not wildcard
data
- lifecycle policy exists for temporary objects
- soft delete and versioning are evaluated
- sensitive metadata is not stored in blob names
operations
- diagnostic settings send logs to Log Analytics
- storage transaction cost is monitored
- upload failure rate is visibleBlob names are also part of the security design. If you save the original file name, such as Hong Gil-dong_Resident Registration Card.png, sensitive information may remain in the URL, log, and metadata. Separate the display name visible to the user from the internal blob path, and make the internal path difficult to guess and prevent conflicts.
avoid
uploads/user-123/passport-photo.png
prefer
uploads/2026/07/05/8f4b1e7c-3d21-4b52-a7f4.binChanging the name to a random number does not complete security, but it serves as a baseline for not leaving unnecessary personal information in logs and URLs.
Points to check during operation
When you connect a web app and storage, there are more points to check.
1. Frontend
- Is the API base URL correct?
- Is CORS allowed for the expected origin?
- Are upload failure messages visible to the user?
2. Backend API
- Are file size limits enforced?
- Are authentication and authorization checked?
- Are blob upload failures retried or compensated?
3. Storage
- Is public access disabled?
- Are container permissions minimized?
- Is a lifecycle policy needed?
4. Security
- Are access keys kept out of code?
- Is Managed Identity or Key Vault used?
- Is the SAS expiration short enough?
5. Observability
- Can App Service logs be inspected?
- Is the upload failure rate tracked?
- Are storage capacity and transaction costs monitored?With Application Insights, you can see request failure rate, response time, and dependency calls.
az monitor app-insights component create \
--app appi-webstorage-prod \
--location koreacentral \
--resource-group rg-web-storage-prodLogs that are particularly useful in operation are upload request ID, user ID, blob name, file size, content type, and storage response code. The original file name may contain personal or sensitive information, so it is better not to leave it in the log.
Cleanup
Connecting Azure App Service and Storage Account does not end with simply “creating a web app and uploading a file.” A user's upload request must go through frontend, API, permission verification, Blob storage, metadata storage, logs, and monitoring to become a function.
Blob storage is convenient, but operational risk increases if the public scope, access key, SAS expiration, CORS, and lifecycle policy are set incorrectly. App Service is also easy to deploy, but you must check environment variables, managed identity, logs, health check, and outbound connections.
Even if it is a small file upload function, if you organize it according to this flow, you can view cloud resources in connection with the screen function. What is more important than how to create resources is to create a structure that can explain where the request originates, where it is stored, and at what layer it can be checked when it fails.
Unauthorized copying, redistribution, and automated scraping are not permitted. Please cite the original URL when referencing this article.