Azure Data Lake Storage Interview Questions and Answers Part 5 - Networking, Data Protection and Recovery

ADLS Gen2 Interview Q&A · Part 5 of 10

In this post we are going to cover networking, data protection and recovery for ADLS Gen2 and Blob Storage. These are 7 real ADLS Gen2 and Blob Storage interview questions for data engineers, asked the way they come up on a real project, and the last one is a scenario: a bank needs a lake that survives deletion mistakes, ransomware and a region outage.

This topic is about two things. First, who can even reach the storage account over the network. Then, how you get data back when something goes wrong: a folder deleted by mistake, a leaked key, an attacker, or a whole region going down. Interviewers ask about it because on an ADLS Gen2 lake some well-known Blob Storage features, like versioning, are not available, and many candidates don't know that.

VIDEO COMING SOON

Part 5 of the Real Time Azure Data Lake Storage Interview Questions and Answers series on TechBrothersIT.

The short answer

Say this first

Close the network and anonymous doors with private endpoints for blob and dfs. On an ADLS Gen2 lake, protection means soft delete, container-level WORM, locks and backup copies to another account: no versioning with a hierarchical namespace. GZRS plus failover drills covers a region outage.

Let's set up the project

We run everything against Azurite, Microsoft's open-source Azure Storage emulator, running locally, with small Python scripts that use the azure-storage-blob client and pyarrow. Features the emulator does not support, such as the hierarchical namespace, ACLs, soft delete, immutability and failover, are shown as code. Every script imports common.py, which connects to the account, creates a container on first use and prints errors in a short form.

common.py
import os, textwrap
from azure.storage.blob import BlobServiceClient

CONN = os.environ["AZURE_STORAGE_CONNECTION_STRING"]
svc = BlobServiceClient.from_connection_string(CONN)


def container(name, **kw):              # create once, reuse after
    c = svc.get_container_client(name)
    if not c.exists():
        c.create_container(**kw)
    return c


def show_err(e):
    code = getattr(e.error_code, "value", e.error_code)
    print(" ", type(e).__name__, e.status_code, code)
    first = str(e).split("\n")[0]
    print(textwrap.indent(textwrap.fill(first, 58), "    "))

Question 1: Security wants no public access to the storage account. What networking options do you set up?

I close two doors. The network door: disable public network access and use private endpoints. The anonymous door: disallow blob public access, so no container can ever be opened to the internet.

For a data lake, we create two private endpoints, one for blob and one for dfs, each with its private DNS zone: privatelink.blob.core.windows.net and privatelink.dfs.core.windows.net. Databricks and Data Factory in the VNet reach the account through them, and the internet has no route in. With only the blob endpoint, operations like managing ACLs or creating directories fail.

lock_down_network.sh
# Azure only: not run on the emulator
az storage account update -g rg-lake -n stlakeprod \
  --public-network-access Disabled \
  --allow-blob-public-access false
# a lake needs TWO private endpoints: blob AND dfs
for sub in blob dfs; do
  az network private-endpoint create -g rg-lake \
    -n pe-stlakeprod-$sub --connection-name pe-stlakeprod-$sub \
    --vnet-name vnet-data --subnet snet-pe --group-id $sub \
    --private-connection-resource-id "$LAKE_ID"
  az network private-dns zone create -g rg-lake \
    -n privatelink.$sub.core.windows.net
done   # plus a VNet link and a DNS zone group per endpoint

If you can't go fully private, set the firewall to deny by default and allow only what you need:

Not fully private?Allows
Firewall: VNet rulessubnets with service endpoints
Firewall: IP rulespublic IP ranges
Resource instance rulesAzure resources, same tenant
Network security perimetergenerally available

The anonymous door we can test on Azurite. We run python public_check.py. It sends anonymous requests, with no key, no SAS and no token, to a private raw container and to an exports container made public by mistake, and then makes exports private.

public_check.py
import requests
from common import container

raw = container("raw")                   # private: the default
raw.upload_blob("finance/gl_2026_10.csv", "acct,amount\n4000,125.5\n",
                overwrite=True)
share = container("exports", public_access="container")  # a mistake
share.upload_blob("customers.csv", "id,email\n1,ana@example.com\n",
                  overwrite=True)


def anonymous(c, blob):                  # no key, no SAS, no token
    read = requests.get(f"{c.url}/{blob}").status_code
    lst = requests.get(f"{c.url}?restype=container&comp=list")
    return f"read {read} | list {lst.status_code}"


print("raw, private    :", anonymous(raw, "finance/gl_2026_10.csv"))
print("exports, public :", anonymous(share, "customers.csv"))
share.set_container_access_policy(signed_identifiers={},
                                  public_access=None)   # the fix
print("exports, fixed  :", anonymous(share, "customers.csv"))
Terminal
$ python public_check.py
raw, private    : read 403 | list 403
exports, public : read 200 | list 200
exports, fixed  : read 403 | list 403

The private raw container refuses anonymous requests with read 403 | list 403. That is the emulator; on Azure the code depends on the account setting and request version. The public exports container returns read 200 | list 200, so anyone can read the customer file. After the fix it is back to 403.

Pitfall

A private endpoint alone doesn't block the public endpoint. Turn public access off, and test from outside the network.

Question 2: Someone deletes a folder of raw files by mistake. How do soft delete and versioning help you recover?

Soft delete keeps deleted data for a retention you choose, 1 to 365 days, and Microsoft recommends at least 7. On a Gen2 account, deleting a directory soft deletes its contents, and one undelete restores the directory, its files and their ACLs.

The catch: versioning isn't supported with a hierarchical namespace. So on a Gen2 lake, soft delete covers deletes, not overwrites.

ADLS Gen2 (HNS)Flat Blob account
Blob soft deletea directory with its contentsblob by blob
Container soft deleteyesyes
Versioningnot supportedyes
Overwrites capturedno: deletes onlyyes

Azurite has no soft delete, but it shows the mistake on a flat namespace. We run python land_raw.py to land six files in two daily folders, then python oops_delete.py to "delete the folder" for October 30th.

land_raw.py
from common import container

raw = container("raw")
for day in ["2026-10-29", "2026-10-30"]:
    for part in range(3):
        raw.upload_blob(f"sales/{day}/part-{part}.csv",
                        f"order_id,amount\n{part},10.50\n",
                        overwrite=True)
names = list(raw.list_blob_names(name_starts_with="sales/"))
print("raw/sales:", len(names), "files in 2 daily folders")
oops_delete.py
from azure.core.exceptions import ResourceNotFoundError
from common import svc, show_err

raw = svc.get_container_client("raw")
folder = "sales/2026-10-30/"
try:
    raw.delete_blob(folder.rstrip("/"))      # "delete the folder"
except ResourceNotFoundError as e:
    print("delete the folder name:"); show_err(e)
names = list(raw.list_blob_names(name_starts_with=folder))
codes = [r.status_code for r in raw.delete_blobs(*names)]
print("deleted", len(names), "blobs, status codes:", codes)
print("left in sales/:")
for name in raw.list_blob_names(name_starts_with="sales/"):
    print("  ", name)
Terminal
$ python land_raw.py
raw/sales: 6 files in 2 daily folders
$ python oops_delete.py
delete the folder name:
  ResourceNotFoundError 404 BlobNotFound
    The specified blob does not exist.
deleted 3 blobs, status codes: [202, 202, 202]
left in sales/:
   sales/2026-10-29/part-0.csv
   sales/2026-10-29/part-1.csv
   sales/2026-10-29/part-2.csv

Deleting the folder name fails with 404 BlobNotFound: there is no folder object. The folder is three blobs, removed with three deletes in one batch, [202, 202, 202]. Only the three files for 2026-10-29 are left, and here the others are gone for good.

On Azure, with soft delete on beforehand, you list the deleted paths and undelete the folder by its deletion ID. Container soft delete covers whole containers.

soft_delete.sh
# Azure only: turn it on BEFORE the bad day
az storage account blob-service-properties update \
  -g rg-lake -n stlakeprod \
  --enable-delete-retention true --delete-retention-days 14 \
  --enable-container-delete-retention true \
  --container-delete-retention-days 14
# HNS lake: find the deleted folder, then undelete it
az storage fs list-deleted-path -f raw --account-name stlakeprod \
  --path-prefix sales/2026-10-30 --auth-mode login
az storage fs undelete-path -f raw --account-name stlakeprod \
  --deleted-path-name sales/2026-10-30 \
  --deletion-id "$DELETION_ID" --auth-mode login
Real project

I keep at least 14 days, so a mistake found after a long weekend is still recoverable. And soft-deleted data is billed like active data.

Question 3: Audit data must never be changed or deleted for seven years. How does immutable storage work?

Immutable storage is write once, read many (WORM). A time-based retention policy lets blobs be created and read, but not modified or deleted until the period ends. A legal hold does the same until someone clears it, for investigations.

Time-based retentionLegal hold
Blocksmodify and deletemodify and delete
Endswhen the period expireswhen the hold is cleared
Typical useaudit records: 7 yearsinvestigations
Scope on Gen2container levelcontainer level

On a Gen2 lake, use container-level policies, because version-level WORM needs versioning, which hierarchical namespace accounts lack. Seven years, with up to two leap days, is 2,557 days, counted from each blob's creation.

Let's go through the lifecycle of a policy:

  • Unlocked first: to test it.
  • Locked: no delete, no shortening.
  • Extend only: at most 5 times.
  • Container: deleted only when empty.

The emulator doesn't implement immutability, so this is code.

audit_worm.sh
# Azure only: not run on the emulator
# 7 years + 2 leap days, counted from each blob's creation
az storage container immutability-policy create \
  -g rg-lake --account-name stauditlake -c audit \
  --period 2557
# test it unlocked; then lock it (extend only, max 5 times)
az storage container immutability-policy lock \
  -g rg-lake --account-name stauditlake -c audit \
  --if-match "$POLICY_ETAG"
# an investigation: legal hold until it is cleared
az storage container legal-hold set -g rg-lake \
  --account-name stauditlake -c audit --tags case2026

For regulated industries, Microsoft had it assessed by Cohasset for SEC 17a-4(f), FINRA 4511 and CFTC 1.31(c)-(d).

Pitfall

Lifecycle delete rules skip blobs in immutable containers, and Unity Catalog external locations can't use WORM containers. So I keep audit data in its own container, or its own account.

Question 4: A teammate asks what happens to the lake if a whole region goes down. How do geo-redundancy and failover work?

It depends on the redundancy. LRS and ZRS stay in one region, so a regional outage means waiting. GRS and GZRS also copy data asynchronously to a secondary region, hundreds of miles away, and a failover makes it primary.

LRSZRSGRS / RA-GRSGZRS / RA-GZRS
A node failsOKOKOKOK
A zone failsdownOKwrites need failoverOK
The region failsdowndownfailoverfailover

Microsoft recommends ZRS in the primary region for ADLS, so for regional protection I pick GZRS: three availability zones in the primary, and an LRS copy in the secondary, readable with RA-GZRS if reports must keep reading. Archive isn't available on ZRS, GZRS or RA-GZRS.

Now the two kinds of failover:

  • Unplanned: the copy is asynchronous, so some data loss is likely; check Last Sync Time first. Afterwards the account is LRS, and geo-redundancy must be set up again. It typically takes about an hour.
  • Planned: generally available since November 2025, it swaps the regions with no data loss expected while both are up, and keeps geo-redundancy. That is your disaster recovery drill.

Redundancy and failover are not emulated, so this is code.

failover.sh
# Azure only: redundancy and failover are not emulated
az storage account create -g rg-lake -n stlakeprod -l eastus2 \
  --kind StorageV2 --hns true --sku Standard_RAGZRS
# how far behind is the secondary?
az storage account show -g rg-lake -n stlakeprod \
  --expand geoReplicationStats \
  --query geoReplicationStats.lastSyncTime
# DR drill, both regions up: no data loss expected
az storage account failover -g rg-lake -n stlakeprod \
  --failover-type Planned
Pitfall

Storage survives, pipelines don't. Have compute ready in the second region, and remember that reading the secondary privately needs its own private endpoint.

Question 5: Someone asks how to back up a data lake. What do you tell them?

First, replication isn't backup: ZRS or GZRS copy a bad delete or an encrypted file as faithfully as good data. A backup is a separate copy that the same mistake, or attacker, can't reach.

On a Gen2 lake that means soft delete for quick undo, a delete lock on the account, and real copies on a schedule: Azure Backup's vaulted backup, generally available for ADLS since November 2025, or AzCopy and Data Factory jobs to a second account with separate credentials.

Let's try the copy approach. We run three commands. First, python backup.py copies the curated container to a second local account, partnerlake, into a container named for the day, reading the source through a read-only SAS. Then python bad_day.py plays the bad day, and python restore.py restores. copytool.py holds the shared copy function.

copytool.py
from datetime import datetime, timezone
from azure.storage.blob import (ContainerSasPermissions,
                                generate_container_sas)


def copy_all(src, dst):
    """Server-side copy of every blob in container src into dst."""
    sas = generate_container_sas(
        src.account_name, src.container_name,
        account_key=src.credential.account_key,
        permission=ContainerSasPermissions(read=True),
        expiry=datetime(2030, 1, 1, tzinfo=timezone.utc))
    if not dst.exists():
        dst.create_container()
    status = []
    for name in src.list_blob_names():
        copy = dst.get_blob_client(name).start_copy_from_url(
            f"{src.url}/{name}?{sas}")       # read-only source SAS
        status.append(copy["copy_status"])
    return status
backup.py
import os
from azure.storage.blob import BlobServiceClient
from common import container
from copytool import copy_all

curated = container("curated")
for t in ["customers", "orders", "products"]:
    curated.upload_blob(f"{t}/part-0.csv", f"table,version\n{t},v1\n",
                        overwrite=True)
backup = BlobServiceClient.from_connection_string(
    os.environ["PARTNER_STORAGE_CONNECTION_STRING"])   # 2nd account
target = backup.get_container_client("curated-2026-10-30")
print("backup to", backup.account_name, "/", target.container_name)
print("copy status:", copy_all(curated, target))
bad_day.py
from common import svc

curated = svc.get_container_client("curated")
curated.delete_blobs(*curated.list_blob_names())   # a bad script
curated.upload_blob("orders/part-0.csv", "ENCRYPTED BY ATTACKER",
                    overwrite=True)                 # or ransomware
print("curated now:", sorted(curated.list_blob_names()))
restore.py
import os
from azure.storage.blob import BlobServiceClient
from common import svc
from copytool import copy_all

backup = BlobServiceClient.from_connection_string(
    os.environ["PARTNER_STORAGE_CONNECTION_STRING"])
src = backup.get_container_client("curated-2026-10-30")
dst = svc.get_container_client("curated-restored")  # never overwrite
print("restore status:", copy_all(src, dst))
for name in sorted(dst.list_blob_names()):
    row = dst.download_blob(name).readall().decode().splitlines()[1]
    print("  ", name, "->", row)
Terminal
$ python backup.py
backup to partnerlake / curated-2026-10-30
copy status: ['success', 'success', 'success']
$ python bad_day.py
curated now: ['orders/part-0.csv']
$ python restore.py
restore status: ['success', 'success', 'success']
   customers/part-0.csv -> customers,v1
   orders/part-0.csv -> orders,v1
   products/part-0.csv -> products,v1

The backup lands in partnerlake / curated-2026-10-30 with copy status: ['success', 'success', 'success']. Then a script deletes everything, and an attacker writes an encrypted orders file, so curated now: ['orders/part-0.csv']. Restore copies the backup into a new container, never over the damaged one, and all three tables come back at v1.

On Azure, a cross-account copy can stay pending, so poll the copy status; the emulator completes at once. And while vaulted backup is on, undelete from soft delete is blocked.

Real project

Test restores regularly. A backup you have never restored is only a hope.

Question 6: A storage account key leaked. What do you do first?

Rotate it, today. An account has two keys, so the apps can keep running on one while we regenerate the other. In order:

  • Move apps: to the other key, or better to Microsoft Entra ID.
  • Regenerate: the leaked key, so its old value stops working.
  • Investigate: StorageBlobLogs, account key and SAS requests by caller IP.
  • Fix the cause: disable shared key access, scan the repos.

Azurite's key is fixed, so we play the regenerated key with a value the account doesn't have. We run python key_rotation.py.

key_rotation.py
import base64
from datetime import datetime, timezone
from azure.storage.blob import (BlobClient, BlobSasPermissions,
                                BlobServiceClient, generate_blob_sas)
from common import CONN, svc, container, show_err

blob = container("finance").upload_blob(
    "gl/2026-10.csv", "acct,amount\n", overwrite=True)
# key1 leaked and was regenerated: its old value is now a wrong key
old_key = base64.b64encode(b"leaked-key1-value" * 4).decode()
stale = BlobServiceClient.from_connection_string(   # app not updated
    CONN.replace(svc.credential.account_key, old_key))
try:
    stale.get_container_client("finance").get_container_properties()
except Exception as e:
    print("app with the old key:"); show_err(e)
sas = generate_blob_sas(svc.account_name, "finance", "gl/2026-10.csv",
      account_key=old_key, permission=BlobSasPermissions(read=True),
      expiry=datetime(2030, 1, 1, tzinfo=timezone.utc))
try:
    BlobClient.from_blob_url(f"{blob.url}?{sas}").download_blob()
except Exception as e:
    print("SAS signed with the old key:"); show_err(e)
data = blob.download_blob().readall()
print("app with the current key: read", len(data), "bytes")
Terminal
$ python key_rotation.py
app with the old key:
  HttpResponseError 403 AuthorizationFailure
    Server failed to authenticate the request. Make sure the
    value of the Authorization header is formed correctly
    including the signature.
SAS signed with the old key:
  HttpResponseError 403 AuthorizationFailure
    Server failed to authenticate the request. Make sure the
    value of the Authorization header is formed correctly
    including the signature.
app with the current key: read 12 bytes

The app still holding the old key gets 403 AuthorizationFailure, and so does a SAS signed with it. On Azure, a wrong key or SAS signature gives 403 AuthenticationFailed. The app with the current key still reads the file: read 12 bytes.

Next, find out what the key was used for. In the StorageBlobLogs table, filter on account key and SAS requests, grouped by caller IP address and operation.

who_used_the_key.kql
// Log Analytics: key and SAS calls; CallerIpAddress is ip:port
StorageBlobLogs
| where TimeGenerated > ago(7d)
| where AuthenticationType in ("AccountKey", "SAS")
| extend ClientIp = extract(@"^(.*):\d+$", 1, CallerIpAddress)
| summarize requests = count()
    by ClientIp, AuthenticationType, OperationName
| order by requests desc

Then fix the cause. Disable shared key access, so account keys and every SAS signed with them stop working, while a user delegation SAS keeps working. Azure Policy can deny accounts that allow shared key.

rotate_key.sh
# Azure only. 1) apps moved to key2 or Entra ID, then:
az storage account keys renew -g rg-lake -n stlakeprod --key key1
# 2) later key2 too; 3) stop key-based auth for good
az storage account update -g rg-lake -n stlakeprod \
  --allow-shared-key-access false
Later

Regenerate the second key too, and scan your repositories for secrets. The next leak usually comes from the same place.

Scenario 7: A bank needs a lake that survives deletion mistakes, ransomware and a region outage. What do you set up?

I answer threat by threat, and then add the baseline underneath.

ThreatWhat I set up
Deletion mistakesblob + container soft delete, a delete lock, least privilege
Ransomwarecontainer WORM, vaulted backup or a 2nd account, shared key off, Defender for Storage
Region outageGZRS (RA if reports must run), planned failover drills, compute ready
Underneathno public access, private endpoints for blob + dfs, Entra ID, policy as code

First, deletion mistakes: blob and container soft delete, a delete lock on the account, and least privilege, so few people can delete at all.

Then ransomware: immutable containers for records that must never change, a vaulted backup or a copy in a separate account, shared key access disabled, and Defender for Storage for activity monitoring and malware scanning on upload.

Then the region outage: GZRS, with read access if reports must keep running, planned failover drills, and compute ready in the secondary region.

And underneath it all: no public network access, private endpoints for blob and dfs, and Microsoft Entra ID with roles and ACLs, deployed as code, with Azure Policy so nobody quietly turns it off. Here is the baseline for the bank's lake account in Azure CLI:

bank_lake_baseline.sh
# Azure only: the baseline for the bank's lake account
az storage account blob-service-properties update \
  -g rg-bank -n stbanklake \
  --enable-delete-retention true --delete-retention-days 30 \
  --enable-container-delete-retention true \
  --container-delete-retention-days 30
az lock create -n no-delete -t CanNotDelete -g rg-bank \
  --resource stbanklake \
  --resource-type Microsoft.Storage/storageAccounts
az storage account update -g rg-bank -n stbanklake \
  --allow-shared-key-access false --public-network-access Disabled
Pitfall

At a bank, people count on versioning or point-in-time restore. Neither works on a hierarchical namespace account, so prove your recovery with restore drills instead.

Key points to remember

  • Close two doors: public network access off + private endpoints for blob and dfs; no anonymous access
  • On Gen2: soft delete (blob + container); no versioning with a hierarchical namespace
  • Container-level WORM: time-based retention or legal hold; test unlocked, then lock
  • GZRS for regional protection; planned failover drills; unplanned ends as LRS
  • Back up to a vault or a separate account; a leaked key: rotate, investigate, disable shared key

The full script

Here is a Python script that writes every file and runs every command in order against Azurite; run pip install azure-storage-blob azure-storage-file-datalake pyarrow pandas first.

Show the full script that writes the files and runs every step
Full script - writes the files and runs every step against Azurite (pip install azure-storage-blob azure-storage-file-datalake pyarrow pandas), run it with Python
# ========================================================================
# ADLS Gen2 Interview Q&A #5 - Networking, data protection and recovery
# TechBrothersIT. Names and sample data are invented for teaching.
# Every step below was run on Azurite 3.37.0 (Microsoft's open-source Azure Storage emulator, Blob service)
# with azure-storage-blob 12.31.1, pyarrow and pandas; the outputs in the video and the blog post are the real outputs.
# To run it yourself:
#   1. npm install -g azurite   then   azurite-blob --skipApiVersionCheck --location ./azurite-data
#      (or the Docker image mcr.microsoft.com/azure-storage/azurite), and leave it running on port 10000.
#      --skipApiVersionCheck lets a newer azure-storage-blob talk to it.
#      This video also uses a second local account: start Azurite with the environment variable
#      AZURITE_ACCOUNTS="devstoreaccount1:Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;partnerlake:cGFydG5lcmxha2Vsb2NhbGtleWZvcmF6dXJpdGVkZW1vMDEyMzQ1Njc4OTAxMjM0NTY3OA=="
#   2. pip install azure-storage-blob azure-storage-file-datalake pyarrow pandas requests
#   3. Set AZURE_STORAGE_CONNECTION_STRING to "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
#      and PARTNER_STORAGE_CONNECTION_STRING to "DefaultEndpointsProtocol=http;AccountName=partnerlake;AccountKey=cGFydG5lcmxha2Vsb2NhbGtleWZvcmF6dXJpdGVkZW1vMDEyMzQ1Njc4OTAxMjM0NTY3OA==;BlobEndpoint=http://127.0.0.1:10000/partnerlake;"
#   then run python ADLS_Interview_QA_05_Code.py.
# Run it in an empty folder and on a freshly started emulator: it creates its files (and any local data
# files) there, and a second run on old data gives different output.
# Steps that restart the emulator say so in a comment: stop and start it by hand at that point.
# ========================================================================

import os, subprocess, time

def write(path, text):
    if text is None:
        if os.path.exists(path): os.remove(path)
        return
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(text.strip("\n") + "\n")

def run(cmd, expect_error=False, allow_error=False, env=None):
    print("\n$ " + cmd, flush=True)
    rc = subprocess.run(cmd, shell=True, env=dict(os.environ, **(env or {}))).returncode
    if rc != 0 and not (expect_error or allow_error):
        raise SystemExit("stopped: this command was expected to succeed")

# Step 0: the starting files
write('common.py', 'import os, textwrap\nfrom azure.storage.blob import BlobServiceClient\n\nCONN = os.environ["AZURE_STORAGE_CONNECTION_STRING"]\nsvc = BlobServiceClient.from_connection_string(CONN)\n\n\ndef container(name, **kw):              # create once, reuse after\n    c = svc.get_container_client(name)\n    if not c.exists():\n        c.create_container(**kw)\n    return c\n\n\ndef show_err(e):\n    code = getattr(e.error_code, "value", e.error_code)\n    print(" ", type(e).__name__, e.status_code, code)\n    first = str(e).split("\\n")[0]\n    print(textwrap.indent(textwrap.fill(first, 58), "    "))\n')

# Step 1: Anonymous access: a private container, a container made public by mistake, and the fix
write('public_check.py', 'import requests\nfrom common import container\n\nraw = container("raw")                   # private: the default\nraw.upload_blob("finance/gl_2026_10.csv", "acct,amount\\n4000,125.5\\n",\n                overwrite=True)\nshare = container("exports", public_access="container")  # a mistake\nshare.upload_blob("customers.csv", "id,email\\n1,ana@example.com\\n",\n                  overwrite=True)\n\n\ndef anonymous(c, blob):                  # no key, no SAS, no token\n    read = requests.get(f"{c.url}/{blob}").status_code\n    lst = requests.get(f"{c.url}?restype=container&comp=list")\n    return f"read {read} | list {lst.status_code}"\n\n\nprint("raw, private    :", anonymous(raw, "finance/gl_2026_10.csv"))\nprint("exports, public :", anonymous(share, "customers.csv"))\nshare.set_container_access_policy(signed_identifiers={},\n                                  public_access=None)   # the fix\nprint("exports, fixed  :", anonymous(share, "customers.csv"))\n')
run('python public_check.py')

# Step 2: Deleting a "folder" on a flat namespace: no folder object, one delete per blob, no soft delete on the emulator
write('land_raw.py', 'from common import container\n\nraw = container("raw")\nfor day in ["2026-10-29", "2026-10-30"]:\n    for part in range(3):\n        raw.upload_blob(f"sales/{day}/part-{part}.csv",\n                        f"order_id,amount\\n{part},10.50\\n",\n                        overwrite=True)\nnames = list(raw.list_blob_names(name_starts_with="sales/"))\nprint("raw/sales:", len(names), "files in 2 daily folders")\n')
write('oops_delete.py', """\
from azure.core.exceptions import ResourceNotFoundError
from common import svc, show_err

raw = svc.get_container_client("raw")
folder = "sales/2026-10-30/"
try:
    raw.delete_blob(folder.rstrip("/"))      # "delete the folder"
except ResourceNotFoundError as e:
    print("delete the folder name:"); show_err(e)
names = list(raw.list_blob_names(name_starts_with=folder))
codes = [r.status_code for r in raw.delete_blobs(*names)]
print("deleted", len(names), "blobs, status codes:", codes)
print("left in sales/:")
for name in raw.list_blob_names(name_starts_with="sales/"):
    print("  ", name)
""")
run('python land_raw.py')
run('python oops_delete.py')

# Step 3: Backup to a second account with server-side copies, a bad day, and a restore into a new container
write('copytool.py', 'from datetime import datetime, timezone\nfrom azure.storage.blob import (ContainerSasPermissions,\n                                generate_container_sas)\n\n\ndef copy_all(src, dst):\n    """Server-side copy of every blob in container src into dst."""\n    sas = generate_container_sas(\n        src.account_name, src.container_name,\n        account_key=src.credential.account_key,\n        permission=ContainerSasPermissions(read=True),\n        expiry=datetime(2030, 1, 1, tzinfo=timezone.utc))\n    if not dst.exists():\n        dst.create_container()\n    status = []\n    for name in src.list_blob_names():\n        copy = dst.get_blob_client(name).start_copy_from_url(\n            f"{src.url}/{name}?{sas}")       # read-only source SAS\n        status.append(copy["copy_status"])\n    return status\n')
write('backup.py', 'import os\nfrom azure.storage.blob import BlobServiceClient\nfrom common import container\nfrom copytool import copy_all\n\ncurated = container("curated")\nfor t in ["customers", "orders", "products"]:\n    curated.upload_blob(f"{t}/part-0.csv", f"table,version\\n{t},v1\\n",\n                        overwrite=True)\nbackup = BlobServiceClient.from_connection_string(\n    os.environ["PARTNER_STORAGE_CONNECTION_STRING"])   # 2nd account\ntarget = backup.get_container_client("curated-2026-10-30")\nprint("backup to", backup.account_name, "/", target.container_name)\nprint("copy status:", copy_all(curated, target))\n')
write('bad_day.py', """\
from common import svc

curated = svc.get_container_client("curated")
curated.delete_blobs(*curated.list_blob_names())   # a bad script
curated.upload_blob("orders/part-0.csv", "ENCRYPTED BY ATTACKER",
                    overwrite=True)                 # or ransomware
print("curated now:", sorted(curated.list_blob_names()))
""")
write('restore.py', """\
import os
from azure.storage.blob import BlobServiceClient
from common import svc
from copytool import copy_all

backup = BlobServiceClient.from_connection_string(
    os.environ["PARTNER_STORAGE_CONNECTION_STRING"])
src = backup.get_container_client("curated-2026-10-30")
dst = svc.get_container_client("curated-restored")  # never overwrite
print("restore status:", copy_all(src, dst))
for name in sorted(dst.list_blob_names()):
    row = dst.download_blob(name).readall().decode().splitlines()[1]
    print("  ", name, "->", row)
""")
run('python backup.py')
run('python bad_day.py')
run('python restore.py')

# Step 4: A leaked, regenerated key: the old key and a SAS signed with it are rejected
write('key_rotation.py', 'import base64\nfrom datetime import datetime, timezone\nfrom azure.storage.blob import (BlobClient, BlobSasPermissions,\n                                BlobServiceClient, generate_blob_sas)\nfrom common import CONN, svc, container, show_err\n\nblob = container("finance").upload_blob(\n    "gl/2026-10.csv", "acct,amount\\n", overwrite=True)\n# key1 leaked and was regenerated: its old value is now a wrong key\nold_key = base64.b64encode(b"leaked-key1-value" * 4).decode()\nstale = BlobServiceClient.from_connection_string(   # app not updated\n    CONN.replace(svc.credential.account_key, old_key))\ntry:\n    stale.get_container_client("finance").get_container_properties()\nexcept Exception as e:\n    print("app with the old key:"); show_err(e)\nsas = generate_blob_sas(svc.account_name, "finance", "gl/2026-10.csv",\n      account_key=old_key, permission=BlobSasPermissions(read=True),\n      expiry=datetime(2030, 1, 1, tzinfo=timezone.utc))\ntry:\n    BlobClient.from_blob_url(f"{blob.url}?{sas}").download_blob()\nexcept Exception as e:\n    print("SAS signed with the old key:"); show_err(e)\ndata = blob.download_blob().readall()\nprint("app with the current key: read", len(data), "bytes")\n')
run('python key_rotation.py')
Next in this series

Part 6: File Formats and Performance

The sample data on this page is invented for teaching. Every terminal output is the real output of the command above it, run on Azurite 3.37.0, Microsoft's open-source Azure Storage emulator, with azure-storage-blob 12.31.1, pyarrow and pandas. Features the emulator does not support are shown as code. Part of the Real Time Azure Data Lake Storage Interview Questions and Answers series on TechBrothersIT. Features, limits and their status (GA or preview) are current as of October 2026; check the official Azure Storage documentation (learn.microsoft.com/azure/storage) for anything you quote in an interview.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.