Разобраться, какие в облаке есть сервисы, — половина дела. Вторая половина — обратиться к ним из кода так, чтобы это было безопасно (без секретов в репозитории) и тестируемо (без реального облака в тестах). Разберём это на четырёх стеках, Spring Boot, Go, Node и Python: переключатель языка над примерами. Прежде чем читать, полезно понимать модель доступа из статьи про Entra ID и RBAC — весь фокус тут в том, откуда приложение берёт права.
DefaultAzureCredential идёт по цепочке и берёт первый найденный источник: в облаке это удостоверение платформы, на ноутбуке вход через командную строку, а забытая переменная окружения перебивает и то и другое. В боевом контуре источник называют явно, чтобы не платить за перебор.
Azure SDK и надстройки фреймворков
К сервисам Azure обращаются через официальный SDK своего языка; в каждом из них клиент собирается один раз на приложение и получает адрес сервиса из настроек, а учётные данные из единой цепочки DefaultAzureCredential.
В Java это Azure SDK for Java, а в Spring-приложениях удобнее набор стартеров Spring Cloud Azure: он подключает Blob Storage, Service Bus, Key Vault и другие сервисы через конфигурацию и внедрение зависимостей.
@Bean
BlobServiceClient blobServiceClient(@Value("${app.storage.endpoint}") String endpoint) {
return new BlobServiceClientBuilder()
.endpoint(endpoint)
.credential(new DefaultAzureCredentialBuilder().build())
.buildClient();
}
Стартеры Spring Cloud Azure делают ровно это за вас: достаточно указать spring.cloud.azure.storage.blob.endpoint в конфигурации, и готовый клиент приезжает бином.
В Go это модули github.com/Azure/azure-sdk-for-go/sdk/...: azidentity для учётных данных и по пакету на сервис (azblob, azservicebus, azsecrets). Стартеров нет, клиента собирают в main и передают дальше зависимостью:
func newBlobClient(cfg Config) (*azblob.Client, error) {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, err
}
return azblob.NewClient(cfg.StorageEndpoint, cred, nil)
}
В Node это пакеты @azure/identity и по пакету на сервис (@azure/storage-blob, @azure/service-bus, @azure/keyvault-secrets); клиента создают один раз при старте:
const { DefaultAzureCredential } = require('@azure/identity');
const { BlobServiceClient } = require('@azure/storage-blob');
function blobServiceClient(config) {
return new BlobServiceClient(config.storageEndpoint, new DefaultAzureCredential());
}
В Python это azure-identity и по пакету на сервис (azure-storage-blob, azure-servicebus, azure-keyvault-secrets); клиента создают один раз при старте:
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
def blob_service_client(settings: Settings) -> BlobServiceClient:
return BlobServiceClient(settings.storage_endpoint, credential=DefaultAzureCredential())
Обратите внимание: адрес хранилища приходит из настроек, а не вписан в код. В тестах туда подставляют локальный эмулятор, в бою — настоящий адрес, и пересобирать ничего не надо.
Управляемые PostgreSQL и Redis вообще не требуют «облачного» SDK: к ним подключаются стандартными драйверами (pgx или JDBC для PostgreSQL, go-redis, Lettuce, ioredis или redis-py для Redis) по адресу managed-кластера — как к любой базе. Пароль при этом можно не заводить вовсе: и Flexible Server, и управляемый Redis умеют аутентификацию через Entra ID, где вместо пароля подставляется временный токен вашей managed identity. Настраивается это один раз, зато из системы уходит ещё один вечный секрет.
DefaultAzureCredential: откуда берутся права
Ключ, положенный в настройки «на время отладки», уезжает в git и живёт там годами — так утекает большинство учётных данных. Поэтому принцип один: никаких секретов в коде, а за получение доступа отвечает DefaultAzureCredential. Важно понимать, что она не «подстраивается под обстановку», а перебирает источники в жёстко заданном порядке и берёт первый, который вообще выдал токен:
- переменные окружения —
AZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_CLIENT_SECRET, если они заданы; - managed identity платформы — виртуальной машины, Container Apps, AKS: платформа выдаёт временный токен, в коде ни пароля, ни ключа;
- учётка разработчика — та, под которой вы вошли через Azure CLI (
az login).
Порядок здесь и есть источник самой популярной отладочной загадки. Переменные окружения стоят первыми, поэтому забытые в окружении AZURE_CLIENT_ID и AZURE_CLIENT_SECRET перебивают managed identity, и приложение в облаке ходит в сервисы под чужой учётной записью. Если «в облаке почему-то не те права» — первым делом смотрите, что лежит в переменных окружения.
Один и тот же код работает и локально, и в облаке — меняется только источник учётных данных. Это избавляет от соблазна захардкодить ключ «на время отладки».
Отдельно про конвейер сборки вне Azure. Напрашивается service principal с секретом в хранилище конвейера, но это ровно тот вечный секрет, от которого стоит уходить. У Azure для этого есть федеративные учётные данные (workload identity federation): конвейер предъявляет свой собственный токен, Entra ID меняет его на короткоживущий доступ, и хранить в настройках конвейера нечего. Тот же приём, что OIDC-федерация в AWS, — о ней рассказывает статья про состояние и доставку.
Права на управление и права на данные — разные
Самая частая ошибка доступа в Azure выглядит так: роль выдали, приложение всё равно получает отказ. Причина почти всегда одна.
Роли уровня управления (Reader, Contributor, Owner) дают права на ресурс как объект: увидеть его в списке, изменить настройки, удалить. Они не дают права на данные внутри.
Роли уровня данных — отдельный набор, и именно они нужны приложению:
| Что нужно приложению | Какая роль |
|---|---|
| Читать файлы из хранилища | Storage Blob Data Reader |
| Писать файлы | Storage Blob Data Contributor |
| Читать секреты из хранилища секретов | Key Vault Secrets User |
| Читать сообщения из очереди служебной шины | Azure Service Bus Data Receiver |
| Отправлять сообщения | Azure Service Bus Data Sender |
То есть Contributor на учётную запись хранения позволяет её настроить и удалить, но не прочитать из неё файл. И наоборот: Storage Blob Data Reader позволяет читать файлы и не позволяет увидеть учётную запись в списке ресурсов.
Отсюда практическое правило: приложению выдают роли данных, и только их, на конкретный ресурс (а не на группу ресурсов или подписку). Права управления приложению обычно не нужны вовсе — если они понадобились, это повод спросить, зачем приложение меняет инфраструктуру.
Проверять удобно так: az role assignment list --assignee <id-удостоверения> --all -o table и глазами посмотреть, есть ли в списке роль со словом Data. Если нет — отказ будет, сколько бы Contributor ни было выдано.
Надстройки фреймворков: настройки вместо кода
Надстройка над библиотеками называется Spring Cloud Azure, и её польза в том, что клиенты и учётные данные собираются из настроек, а не из кода.
spring:
cloud:
azure:
credential:
managed-identity-enabled: true
client-id: ${AZURE_CLIENT_ID} # для удостоверения, назначенного пользователем
profile:
tenant-id: ${AZURE_TENANT_ID}
storage:
blob:
account-name: stordersprod
servicebus:
namespace: sb-orders-prod
entity-type: queue
keyvault:
secret:
property-sources:
- endpoint: https://kv-orders-prod.vault.azure.net/
Что это даёт:
Готовые бины клиентов. BlobServiceClient, клиент служебной шины, клиент хранилища секретов создаются автоматически и внедряются как обычные зависимости — без сборщиков в коде.
Хранилище секретов как источник свойств. Последний блок в примере подключает секреты как обычные настройки Spring: значение db-password из хранилища доступно как ${db-password} в любом месте конфигурации, включая пароль к базе. Никаких обращений к хранилищу в коде.
Слушатель очереди в привычном виде. Есть поддержка обработчиков сообщений служебной шины через привычные аннотации, то есть работа с очередью выглядит как работа с любым другим брокером.
Три вещи, о которых стоит знать заранее. Версия надстройки должна соответствовать версии вашего фреймворка — таблица соответствия есть в документации, и несовпадение проявляется как «свойства не подхватываются». Секреты из хранилища читаются при старте, то есть смена секрета требует перезапуска (или отдельного механизма обновления настроек) — та же оговорка, что и в других облаках. И настройки перекрывают друг друга обычным способом Spring: файл, переменные среды, аргументы — поэтому в боевом контуре значения приходят из окружения, а в файле остаются только имена и адреса.
В Go надстройки над SDK нет, и «настройки вместо кода» делают руками: клиенты собирают в main из конфигурации, которую читают из переменных окружения (envconfig, caarlos0/env или viper), учётные данные один DefaultAzureCredential на процесс, а секреты из Key Vault читают при старте клиентом azsecrets и кладут в ту же структуру конфигурации.
type Config struct {
StorageEndpoint string `env:"APP_STORAGE_ENDPOINT,required"`
ServiceBusNS string `env:"APP_SERVICEBUS_NAMESPACE,required"`
KeyVaultEndpoint string `env:"APP_KEYVAULT_ENDPOINT,required"`
}
func loadSecrets(ctx context.Context, cfg Config, cred azcore.TokenCredential) (dbPassword string, err error) {
client, err := azsecrets.NewClient(cfg.KeyVaultEndpoint, cred, nil)
if err != nil {
return "", err
}
resp, err := client.GetSecret(ctx, "db-password", "", nil)
if err != nil {
return "", err
}
return *resp.Value, nil
}
Секреты читаются при старте, то есть смена секрета требует перезапуска или своего механизма обновления — та же оговорка, что и в других облаках. Слушатель очереди служебной шины это цикл receiver.ReceiveMessages из azservicebus в отдельной горутине с явным CompleteMessage после обработки.
В Node стартеров нет, и «настройки вместо кода» это конфигурация из переменных окружения (process.env через dotenv или convict), один DefaultAzureCredential на процесс и чтение секретов из Key Vault при старте.
const { DefaultAzureCredential } = require('@azure/identity');
const { SecretClient } = require('@azure/keyvault-secrets');
const credential = new DefaultAzureCredential();
async function loadConfig() {
const secrets = new SecretClient(process.env.APP_KEYVAULT_ENDPOINT, credential);
return {
storageEndpoint: process.env.APP_STORAGE_ENDPOINT,
serviceBusNamespace: process.env.APP_SERVICEBUS_NAMESPACE,
dbPassword: (await secrets.getSecret('db-password')).value,
};
}
Секреты читаются при старте, смена требует перезапуска или своего механизма обновления. Слушатель очереди служебной шины это receiver.subscribe({ processMessage, processError }) из @azure/service-bus; сообщение подтверждают completeMessage только после успешной обработки.
В Python роль надстройки играет конфигурация через pydantic-settings: значения приходят из переменных окружения, один DefaultAzureCredential на процесс, секреты из Key Vault дочитывают при старте.
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_storage_endpoint: str
app_servicebus_namespace: str
app_keyvault_endpoint: str
settings = Settings()
credential = DefaultAzureCredential()
db_password = SecretClient(settings.app_keyvault_endpoint, credential).get_secret("db-password").value
Секреты читаются при старте, смена требует перезапуска или своего механизма обновления. Слушатель очереди служебной шины это receiver.receive_messages() из azure-servicebus в цикле с complete_message после успешной обработки.
Тесты и локальная разработка
Два разных вопроса, которые часто путают: «как тестировать без облака» и «как разрабатывать, когда доступа в облако нет вовсе».
Эмулятор хранилища в тестах
Для хранилища есть эмулятор — Azurite. Он поднимается контейнером и понимает тот же интерфейс, что настоящее хранилище: блобы, очереди, таблицы.
@Testcontainers
class BlobStorageTest {
@Container
static final GenericContainer<?> AZURITE =
new GenericContainer<>(DockerImageName.parse("mcr.microsoft.com/azure-storage/azurite:latest"))
.withExposedPorts(10000);
static BlobServiceClient client;
@BeforeAll
static void setUp() {
String endpoint = "http://%s:%d/devstoreaccount1"
.formatted(AZURITE.getHost(), AZURITE.getMappedPort(10000));
client = new BlobServiceClientBuilder()
.endpoint(endpoint)
.connectionString(AZURITE_CONNECTION_STRING) // у эмулятора фиксированные учётные данные
.buildClient();
}
@Test
void сохраняетИЧитает() {
var container = client.createBlobContainer("orders");
container.getBlobClient("a.txt").upload(BinaryData.fromString("hello"));
assertThat(container.getBlobClient("a.txt").downloadContent().toString()).isEqualTo("hello");
}
}
func TestBlobStorage(t *testing.T) {
ctx := context.Background()
azuriteC, err := azurite.Run(ctx, "mcr.microsoft.com/azure-storage/azurite:latest")
testcontainers.CleanupContainer(t, azuriteC)
if err != nil {
t.Fatal(err)
}
endpoint, err := azuriteC.BlobServiceURL(ctx)
if err != nil {
t.Fatal(err)
}
cred, _ := azblob.NewSharedKeyCredential(azurite.AccountName, azurite.AccountKey) // у эмулятора фиксированные учётные данные
client, err := azblob.NewClientWithSharedKeyCredential(endpoint+"/"+azurite.AccountName, cred, nil)
if err != nil {
t.Fatal(err)
}
_, err = client.CreateContainer(ctx, "orders", nil)
if err != nil {
t.Fatal(err)
}
if _, err := client.UploadBuffer(ctx, "orders", "a.txt", []byte("hello"), nil); err != nil {
t.Fatal(err)
}
resp, err := client.DownloadStream(ctx, "orders", "a.txt", nil)
if err != nil {
t.Fatal(err)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != "hello" {
t.Fatalf("прочитали %q", body)
}
}
const { AzuriteContainer } = require('@testcontainers/azurite');
const { BlobServiceClient } = require('@azure/storage-blob');
let azurite, client;
beforeAll(async () => {
azurite = await new AzuriteContainer('mcr.microsoft.com/azure-storage/azurite:latest').start();
client = BlobServiceClient.fromConnectionString(azurite.getConnectionString()); // у эмулятора фиксированные учётные данные
});
afterAll(() => azurite.stop());
test('сохраняет и читает', async () => {
const container = client.getContainerClient('orders');
await container.create();
await container.getBlockBlobClient('a.txt').upload('hello', 5);
const downloaded = await container.getBlobClient('a.txt').downloadToBuffer();
expect(downloaded.toString()).toBe('hello');
});
from azure.storage.blob import BlobServiceClient
from testcontainers.azurite import AzuriteContainer
def test_saves_and_reads():
with AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:latest") as azurite:
client = BlobServiceClient.from_connection_string(azurite.get_connection_string()) # фиксированные учётные данные эмулятора
container = client.create_container("orders")
container.upload_blob("a.txt", b"hello")
assert container.download_blob("a.txt").readall() == b"hello"
Ключевой момент — переключение адреса: в тесте клиент собирается с адресом эмулятора вместо настоящего, и это единственное отличие от боевой конфигурации. Поэтому адрес держат в настройках, а не в коде: тогда тот же код работает и с эмулятором, и с облаком.
У эмулятора фиксированные учётные данные (они одни и те же у всех, это часть его устройства), и это нормально: он не предназначен для чего-либо, кроме тестов.
Чего у эмулятора нет: управляемых удостоверений и ролей (проверка прав в тестах не проверяется), части новых возможностей, точного поведения под нагрузкой. Поэтому им проверяют логику работы с хранилищем, а не настройку доступа.
Для служебной шины полноценного эмулятора долго не было; сейчас есть образ для локального запуска с ограничениями, и часть команд по-прежнему обходится либо подменой интерфейса в коде, либо отдельным пространством имён в облаке под тесты.
Где это применяется
Эта связка — как приложение аутентифицируется через managed identity и ходит в облачные сервисы через SDK — стоит за любым боевым сервисом в Azure. DefaultAzureCredential делает так, что один код работает в любом окружении, а секреты не оседают в репозитории.
Типичные ошибки новичков:
- кладут строку подключения с ключом в git или в код — классическая утечка;
- жёстко прописывают учётные данные вместо DefaultAzureCredential — код не переносится между окружениями;
- тестируют против реального облака вместо Azurite и Testcontainers — тесты медленные и хрупкие;
- дублируют доступ через ключи там, где хватило бы managed identity.
Глубже: что делает перебор источников и почему в проде его сужаютрасширенное
Единая цепочка учётных данных удобна тем, что код одинаков везде. Цена этого удобства — перебор: при первом обращении библиотека по очереди пробует источники, и каждая неудачная попытка это время, а иногда и сетевой запрос.
Порядок примерно такой: переменные окружения → управляемое удостоверение → вход через инструмент разработчика (командная строка Azure, среда разработки) → и так далее. В облаке срабатывает второй пункт, на ноутбуке — третий, и всё, что стоит до сработавшего, отнимает время.
Что из этого следует.
Первый вызов медленнее. Обычно это десятки-сотни миллисекунд, и на старте приложения это незаметно. Проблема появляется там, где источник подвисает: в отдельных окружениях попытка обратиться к службе управляемых удостоверений уходит в таймаут, и запуск приложения затягивается на секунды или десятки секунд. Классика — контейнер вне Azure (на своём железе, в другом облаке, в среде сборки), где такой службы нет.
В боевом контуре источник указывают явно. Если вы знаете, что работаете с управляемым удостоверением, берите именно его — без перебора:
var credential = new ManagedIdentityCredentialBuilder()
.clientId(System.getenv("AZURE_CLIENT_ID")) // для удостоверения, назначенного пользователем
.build();
cred, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
ID: azidentity.ClientID(os.Getenv("AZURE_CLIENT_ID")), // для удостоверения, назначенного пользователем
})
const credential = new ManagedIdentityCredential({ clientId: process.env.AZURE_CLIENT_ID }); // для удостоверения, назначенного пользователем
credential = ManagedIdentityCredential(client_id=os.environ["AZURE_CLIENT_ID"]) # для удостоверения, назначенного пользователем
Токен кэшируется. Один раз полученный токен живёт до истечения срока и обновляется сам, поэтому перебор случается один раз за жизнь процесса — а вот объект учётных данных надо создавать один раз и переиспользовать, а не создавать на каждый вызов.
Часть источников можно отключить. У сборщика цепочки есть флаги, исключающие лишние источники, — это компромисс между единым кодом и предсказуемостью.
Практическая схема, которой пользуются: в локальной разработке — цепочка (работает вход через командную строку), в боевом контуре — явное управляемое удостоверение, выбор между ними по переменной окружения или по профилю приложения.
Глубже: повторы, таймауты и ограничение частотырасширенное
Облачные библиотеки Azure сами повторяют неудачные запросы — и об этом надо знать, чтобы не построить повтор поверх повтора.
Что повторяется само. Ошибки сети, 408, 429 (ограничение частоты) и 5xx. По умолчанию это несколько попыток с нарастающей паузой. Причём для 429 библиотека уважает заголовок с указанием, сколько ждать, — то есть ведёт себя правильнее, чем самописный повтор.
Что не повторяется. Ошибки клиента (400, 403, 404, 409, 412) — повторять их бессмысленно, и это правильно.
Как настроить.
var options = new BlobClientOptions()
.setRetryOptions(new RequestRetryOptions(
RetryPolicyType.EXPONENTIAL, 4, // четыре попытки
Duration.ofSeconds(10), // предел на попытку
Duration.ofMillis(200), Duration.ofSeconds(4), null));
client, err := azblob.NewClient(endpoint, cred, &azblob.ClientOptions{
ClientOptions: azcore.ClientOptions{
Retry: policy.RetryOptions{
MaxRetries: 4, // четыре попытки
TryTimeout: 10 * time.Second, // предел на попытку
RetryDelay: 200 * time.Millisecond,
MaxRetryDelay: 4 * time.Second,
},
},
})
const client = new BlobServiceClient(endpoint, credential, {
retryOptions: {
maxTries: 4, // четыре попытки
tryTimeoutInMs: 10_000, // предел на попытку
retryDelayInMs: 200,
maxRetryDelayInMs: 4_000,
},
});
client = BlobServiceClient(
endpoint,
credential=credential,
retry_total=4, # четыре попытки
connection_timeout=10, # предел на попытку
retry_backoff_factor=0.2,
retry_backoff_max=4,
)
Три правила, которые стоит соблюдать:
Свой повтор сверху не добавляют. Четыре попытки библиотеки плюс три ваших — это двенадцать запросов на одну операцию, и при сбое сервиса это превращается в лавину. Настраивают одно место: либо библиотеку, либо свой механизм с отключёнными встроенными повторами.
Таймаут на попытку обязателен. Без него один зависший запрос держит поток столько, сколько позволит сеть. Предел на попытку плюс число попыток дают предсказуемое худшее время операции — а это то, что нужно для собственных таймаутов и бюджетов.
Ограничение частоты надо видеть. Ответы 429 не видны в приложении (библиотека их проглатывает), но видны как рост задержки. Поэтому включают журналирование библиотеки на уровне предупреждений и смотрят метрики самого сервиса: если 429 много, лечится это не повторами, а поднятием ёмкости или снижением частоты обращений.
Гонять тесты против реального облака — медленно, дорого и требует доступа. Его подменяют локально:
- для Blob Storage, Storage Queues и таблиц поднимают Azurite — официальный локальный эмулятор хранилища — в контейнере через Testcontainers; клиент переключается на него сменой endpoint. Service Bus Azurite не эмулирует: под него у Microsoft есть отдельный эмулятор, тоже запускаемый контейнером;
- для PostgreSQL, Redis используют те же Testcontainers с обычными образами — managed-сервис в тестах не нужен, драйвер один и тот же.
Так интеграционные тесты идут быстро и не зависят от сети и облака. Общие подходы к тестированию — в разделе тестирования.
Глубже: если доступа в Azure нет вовсерасширенное
Вход через командную строку требует учётной записи в каталоге организации, а она есть не у всех — например, у подрядчика или у нового сотрудника в первый день. Что делать, чтобы разработка не останавливалась:
Подменять на границе. Весь доступ к облачным сервисам держат за своими интерфейсами (FileStorage, EventPublisher), а в локальном профиле подставляют реализации, которые пишут в локальный каталог и в память. Это самый надёжный вариант: приложение запускается вообще без облака и без контейнеров.
Эмуляторы там, где они есть. Хранилище — эмулятор, база — обычный PostgreSQL в контейнере, кэш — обычный Redis в контейнере. Для большинства приложений это закрывает всё, кроме специфичных сервисов.
Отдельная песочница вместо боевого каталога. Если облако всё-таки нужно, правильный ответ — не выдавать доступ в общий контур, а завести отдельную подписку-песочницу с ограниченным бюджетом и своими учётными записями. Это работа платформенной команды, но просить стоит именно это, а не доступ «посмотреть».
И то, чего делать не стоит: раздавать строки подключения с ключами доступа «на время разработки». Такой ключ живёт вечно, уезжает в историю команд и в чаты, и отзывать его придётся вместе со сменой во всех приложениях.
Коротко
- Клиент сервиса собирают один раз на приложение, адрес берут из настроек, учётные данные из DefaultAzureCredential; в Spring это стартеры Spring Cloud Azure, в Go, Node и Python по пакету на сервис.
- К управляемым PostgreSQL и Redis подключаются обычными драйверами по адресу, облачный SDK им не нужен.
- Цепочка учётных данных пробует переменные окружения, managed identity и вход разработчика по порядку: забытые AZURE_CLIENT_ID и секрет в окружении перебивают удостоверение платформы; конвейеру вне облака делают федерацию, а не секрет.
- Перебор стоит времени и иногда зависает: в боевом контуре источник указывают явно, объект учётных данных создают один раз, токен кэшируется.
- Роли управления не дают прав на данные: приложению выдают роли данных на конкретный ресурс, а проверяют список назначений по слову Data.
- Библиотеки повторяют сеть, 408, 429 и 5xx сами и уважают заголовок задержки: свой повтор сверху не добавляют, таймаут на попытку обязателен, 429 видят по логам библиотеки и росту задержки.
- Надстройки фреймворка подключают секреты как свойства и слушателей очередей как обработчики, но секреты читаются при старте и требуют перезапуска при смене.
- Тесты идут на Azurite и Testcontainers через смену адреса; эмулятор не проверяет права и не воспроизводит нагрузку. Без доступа в облако подменяют на границе интерфейсами и берут песочницу, а не строку с ключом.
Что почитать дальше
- Entra ID и RBAC — откуда берётся managed identity и роли данных.
- Безопасность и наблюдаемость — как секреты Key Vault попадают в приложение.
- Управляемые данные в Azure — сервисы, к которым подключаются драйверами и SDK.
- Blob Storage — хранилище, которое в тестах заменяет Azurite.