Nos últimos tempos, tenho trabalhado em um sistema OCR voltado para leitura de receitas médicas digitais. O objetivo é simples: permitir que profissionais da saúde e farmácias possam automatizar a interpretação de prescrições médicas a partir de imagens enviadas via API. Tudo isso com segurança, escalabilidade e sem dor de cabeça com gestão de credenciais.

Neste primeiro artigo da série, vou mostrar como pensei a arquitetura, a justificativa do uso de Azure Functions, e como fiz a integração com o Blob Storage usando Managed Identity. Ao final, teremos um endpoint funcional para upload seguro de imagens. Bora lá?


Por que Azure Functions?

A escolha por Azure Functions veio naturalmente por algumas razões:

  • Serverless: não preciso me preocupar com infraestrutura.
  • Escalável: o sistema vai lidar com muitos envios de imagem, então escalar sob demanda é essencial.
  • Integrado com o ecossistema Azure: especialmente Blob Storage e Managed Identity.

E como estamos falando de um sistema de OCR, onde a trigger principal será o envio de uma imagem para análise, uma Function HTTP cai como uma luva.


Sobre a arquitetura do projeto

Organizei a estrutura do código de forma limpa e modular, seguindo uma pegada de DDD light:

/ocr-function-app
├── application/
│   └── UploadImageService.ts
├── domain/
│   └── IImageStorage.ts
├── infrastructure/
│   └── AzureBlobStorage.ts
├── HttpAddToBlob/
│   └── index.ts
│   └── function.json
├── host.json
├── local.settings.json
└── package.json

A ideia é que a Function seja apenas o entrypoint, delegando responsabilidades para camadas mais específicas.


⛏️ Configurando o ambiente da Azure Function

Antes de começar a codar, precisamos garantir que o ambiente da nossa Azure Function esteja pronto para subir e rodar corretamente com autenticação via Managed Identity e integração com o Blob Storage.

  1. Crie sua Azure Function no portal ou via CLI:
func init ocr-function-app --worker-runtime node --language typescript
  1. Crie a Function HTTP trigger:
func new --name HttpAddToBlob --template "HTTP trigger"

📦 Pacotes utilizados

Você vai precisar instalar os seguintes pacotes no seu projeto TypeScript com Azure Functions:

npm install @azure/storage-blob
npm install @azure/identity

Esses pacotes serão usados para:

  • Criar a Function HTTP trigger (@azure/functions)
  • Interagir com o Blob Storage (@azure/storage-blob)
  • Usar autenticação com Managed Identity (@azure/identity)

🔗 Conectando sua Azure Function ao Blob Storage com segurança

A essa altura, já conseguimos montar a base da nossa Function App e temos o código pronto para receber imagens. Agora vem uma parte fundamental: garantir que a aplicação consiga acessar o Azure Blob Storage de forma segura e escalável.

A ideia aqui é evitar o uso de strings de conexão sensíveis no seu código ou variáveis de ambiente, optando por algo muito mais seguro: Managed Identity + Service Connector.

✅ Por que usar o Service Connector?

Quando usamos o DefaultAzureCredential no código, o Azure já sabe que queremos usar uma identidade gerenciada (Managed Identity) para autenticar. Mas pra isso funcionar na prática, precisamos garantir que essa identidade tenha permissão de acesso ao Blob.

O Service Connector entra como um facilitador: ele cria a conexão entre os recursos com segurança e sem complicação, além de cuidar das configurações de rede e permissões pra você.

⚙️ Criando a conexão com o Blob

  • No portal Azure, vá até sua Function App.
  • No menu lateral, clique em Service Connector > + Adicionar. Service Connector
  1. Preencha as opções assim:

Recurso de destino: selecione sua conta do Azure Storage.
Nome da conexão: algo como BlobConnection_OcrApp.

Configuração Inicial

  • Tipo de autenticação: selecione User Assigned Managed Identity (é isso que torna tudo mais seguro).
    Configuração de Autenticação

  • Na etapa de rede, pode deixar as configurações padrão. O Azure se encarrega de garantir que sua Function App consiga se comunicar com o Storage.
    configurações de rede

  • Clique em Próximo: Revisar + Criar e depois em Criar.
    revisar

Esse processo leva alguns minutos, mas quando termina, sua Function já está liberada pra acessar o Blob com toda a segurança que a nuvem pode oferecer.


🔐 Permissões: liberando o acesso ao container com o Service Connector

Depois de criar a conexão, o service connector garante que a identidade gerenciada da Function terá permissão no container do Blob.

E como validar isso?

  1. Acesse a conta de armazenamento no portal Azure.
  2. Vá em Controle de acesso (IAM).
  3. Clique em Role Assignments.
  4. Você verá que a identity usada com as permissões igual ao ocr-umi:

Identidade confirmada

Salve as mudanças. Pronto! Agora sua Function App já pode fazer uploads no Blob com segurança, sem precisar armazenar nenhuma credencial no código.


📦 No código, a implementação é simples!

No código você vai configurar o DefaultAzureCredential. Ele vai automaticamente usar a identidade configurada pelo Service Connector.

const credential = new DefaultAzureCredential({
  managedIdentityClientId,
});

Com isso, seu backend está muito mais seguro — e ainda pronto pra escalar sem dores de cabeça.


🔐 Variáveis de ambiente

As variáveis de ambiente são essenciais para configurar o acesso ao Blob Storage de forma segura usando identidade gerenciada:

local.settings.json (somente para desenvolvimento local)

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AZURE_STORAGEBLOB_RESOURCEENDPOINT": "https://.blob.core.windows.net",
    "AZURE_STORAGEBLOB_CONTAINERNAME": "ocr-container",
    "AZURE_STORAGEBLOB_CLIENTID": ""
  }
}

O AZURE_STORAGEBLOB_CLIENTID deve conter o Client ID da User Assigned Managed Identity criada no Azure, que será usada pela Function para acessar o Blob.


Upload seguro usando Managed Identity

Um ponto crítico aqui é o upload da imagem para o Azure Blob Storage. Mas ao invés de usar uma connection string hardcoded (o que seria um tiro no pé em termos de segurança), optei por usar Managed Identity conforme falei anteriormente.

O fluxo é esse:

  1. A imagem chega via HTTP.
  2. A Function autentica com o Blob Storage via Managed Identity.
  3. A imagem é salva de forma segura no container.
  4. A URL da imagem é retornada para quem chamou a API.

Diagrama Sequência

A mágica começa no serviço de aplicação:

export class UploadImageService {
  constructor(private readonly imageStorage: IImageStorage) {}

  async handleUpload(buffer: Buffer): Promise<{ url: string; fileName: string }> {
    const fileName = `${uuidv4()}.png`;
    const url = await this.imageStorage.uploadImage(buffer, fileName);
    return { url, fileName };
  }
}

Aqui, o UploadImageService segue o princípio de injeção de dependência, trabalhando com a interface IImageStorage, o que facilita testes e desacopla a lógica de negócio da implementação real de armazenamento.


Implementando o armazenamento com Azure Blob

A implementação concreta do IImageStorage é a classe AzureBlobStorage, que encapsula toda a interação com o SDK do Azure.

export class AzureBlobStorage implements IImageStorage {
    private readonly blobServiceClient: BlobServiceClient;

    constructor(
        url: string, 
        private readonly containerName: string, 
        credential: DefaultAzureCredential) {
        this.blobServiceClient = new BlobServiceClient(
            url,
            credential
        );
    }

    async uploadImage(buffer: Buffer, fileName: string): Promise<string> {
        const containerClient = this.blobServiceClient.getContainerClient(this.containerName);
        const blobClient = containerClient.getBlockBlobClient(fileName);
        await blobClient.upload(buffer, buffer.length);
        return blobClient.url;
    }
}

Repare que o BlobServiceClient é instanciado com o DefaultAzureCredential, que automaticamente utiliza a Managed Identity da Function App no Azure, sem expor segredos no código.


A Function que recebe a imagem

Na camada de API, temos uma Azure Function que serve como ponto de entrada para a aplicação:

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { BlobServiceClient } from "@azure/storage-blob";
import { DefaultAzureCredential } from "@azure/identity";

import { AzureBlobStorage } from "../infrastructure/AzureBlobStorage";
import { UploadImageService } from "../application/UploadImageService";

// Carregando variáveis de ambiente
const blobUrl = process.env.BLOB_STORAGE_URL!;
const containerName = process.env.BLOB_CONTAINER_NAME!;
const managedIdentityClientId = process.env.AZURE_STORAGEBLOB_CLIENTID!;

const httpTrigger: AzureFunction = async function (context, req) {
  if (!req.body || !req.headers["content-type"]?.startsWith("image/")) {
    context.res = { status: 400, body: "Imagem inválida ou ausente" };
    return;
  }

  const buffer = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body);

  const credential = new DefaultAzureCredential({
    managedIdentityClientId,
  });

  const blobStorage = new AzureBlobStorage(blobUrl, containerName, credential);

  const uploadService = new UploadImageService(storage);
  const { url } = await uploadService.handleUpload(buffer);

  context.res = {
    status: 200,
    body: {
      message: "Imagem armazenada com sucesso",
      url,
    },
  };
};

No exemplo abaixo mostra a execução através de curl da function na azure:

PoC executada com sucesso

Com poucos blocos de código, conseguimos montar um pipeline de upload completo, limpo e seguro.


Considerações de segurança

Essa abordagem tem várias vantagens:

✅ Nada de connection strings no código

✅ Uso de identidade gerenciada (Managed Identity) para autenticar com o Blob Storage

✅ Organização de código clara, separando a Function da lógica de negócio


🔄 Próximos Passos

Com a imagem armazenada com sucesso, o próximo passo será:

Integrar com o banco de dados Azure SQL, onde vamos modelar e armazenar os dados extraídos da receita médica antes, durante e após o processamento OCR.

Vamos registrar dados como:

  • ID da imagem
  • Data de upload
  • Texto extraído via OCR
  • Status de processamento

Tudo isso mantendo segurança e escalabilidade.

🚀 Próximo passo: Parte 2 - Persistindo dados no Azure SQL com Boas Práticas