<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <docs>https://blogs.law.harvard.edu/tech/rss</docs>
    <title>Azure on FXShell - DevOps &amp; Sec</title>
    <link>https://fxshell.com.br/tags/azure/</link>
    <description>Recent content in Azure on FXShell - DevOps &amp; Sec</description>
    <image>
      <title>Azure on FXShell - DevOps &amp; Sec</title>
      <link>https://fxshell.com.br/tags/azure/</link>
      <url>fxshell.png</url>
    </image>
    <ttl>1440</ttl>
    <generator>Hugo 0.152.2</generator>
    <language>pt-br</language>
    <lastBuildDate>Thu, 14 May 2026 21:17:58 UT</lastBuildDate>
    <atom:link href="https://fxshell.com.br/tags/azure/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Migração S3 - Azure Blob Storage com Ansible e Azure Data Factory</title>
      <link>https://fxshell.com.br/posts/ansible-adf-migrate-s3-azure/</link>
      <pubDate>Fri, 08 May 2026 00:00:00 UT</pubDate>
      <dc:creator>Felipe da Matta</dc:creator>
      <guid>https://fxshell.com.br/posts/ansible-adf-migrate-s3-azure/</guid>
      <description>Em um projeto de migração real, precisamos mover os dados de dezenas de buckets S3 de clientes para o Azure Blob Storage. O processo manual - criar o ADF no portal, configurar cada linked service, montar a pipeline, disparar e monitorar - era repetitivo e propenso a erro. A solução foi um único playbook Ansible que provisiona toda a infraestrutura e executa a cópia de ponta a ponta.
Objetivo do Lab Construir um playbook Ansible que automatiza completamente a migração de dados de um bucket AWS S3 para um container no Azure Blob Storage, usando o Azure Data Factory como motor de cópia. O playbook cria todos os recursos necessários, dispara a pipeline e faz polling do status até confirmar o sucesso (ou falha explícita).
</description>
      <content:encoded><![CDATA[Em um projeto de migração real, precisamos mover os dados de dezenas de buckets S3 de clientes para o Azure Blob Storage. O processo manual - criar o ADF no portal, configurar cada linked service, montar a pipeline, disparar e monitorar - era repetitivo e propenso a erro. A solução foi um único playbook Ansible que provisiona toda a infraestrutura e executa a cópia de ponta a ponta.
Objetivo do Lab Construir um playbook Ansible que automatiza completamente a migração de dados de um bucket AWS S3 para um container no Azure Blob Storage, usando o Azure Data Factory como motor de cópia. O playbook cria todos os recursos necessários, dispara a pipeline e faz polling do status até confirmar o sucesso (ou falha explícita).
Tecnologias Utilizadas Ansible é uma ferramenta de automação de infraestrutura sem agente. Ela executa tarefas em sequência, em formato YAML, sem precisar instalar nada nos servidores de destino. É amplamente usada para provisionar recursos em nuvem via CLI, configurar servidores e orquestrar fluxos de deploy.
Azure Data Factory (ADF) é o serviço de integração de dados da Microsoft. Ele permite criar pipelines visuais (ou via API/CLI) que copiam, transformam e movem dados entre fontes heterogêneas - como AWS S3 e Azure Blob Storage. O ADF escala automaticamente, tem retry nativo e gera logs detalhados para cada execução.
AWS S3 (Simple Storage Service) é o serviço de armazenamento de objetos da Amazon. Os dados ficam organizados em buckets, e o acesso externo é controlado por IAM com AccessKey e SecretKey.
Azure Blob Storage é o equivalente ao S3 na Azure. Dados são armazenados em containers dentro de uma Storage Account. Suporta acesso via connection string, Managed Identity ou SAS token.
az CLI é a interface de linha de comando da Azure. Permite criar e gerenciar recursos diretamente do terminal, sem usar o portal. O Ansible chama o az CLI via módulo command, que é a forma mais direta e sem dependências extras.
Arquitetura ┌─────────────────────────────────────────────────────────────────┐ │ AWS Account │ │ ┌─────────────┐ │ │ │ S3 Bucket │ ← s3:GetObject + s3:GetObjectVersion │ │ └──────┬──────┘ │ └─────────┼───────────────────────────────────────────────────────┘ │ │ HTTPS (LinkedService_S3 - AccessKey) ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Azure (Resource Group) │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Azure Data Factory (ADF) │ │ │ │ │ │ │ │ Dataset_S3 ──[CopyActivity]──► Dataset_Blob │ │ │ │ (BinarySource) (BinarySink) │ │ │ │ parallelCopies=4 / DIU=4 │ │ │ └──────────────────────────┬──────────────────────────────┘ │ │ │ │ │ │ LinkedService_BlobStorage │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Storage Account: stdadosmigrados │ │ │ │ └── Container: dados-migrados │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ▲ │ ansible-playbook playbook.yml -e @vars.yml │ ┌─────────┴──────────┐ │ Control Node │ │ Ansible + az CLI │ └────────────────────┘ Componente Função S3 Bucket Origem dos dados. IAM com s3:GetObject e s3:GetObjectVersion Azure Data Factory Orquestra a cópia. Gerencia linked services, datasets e pipeline LinkedService_S3 Credencial do ADF para autenticar no S3 (AccessKey) LinkedService_Blob Credencial do ADF para gravar no Blob (connection string) Dataset_S3 Define o caminho de origem: bucket + prefixo Dataset_Blob Define o destino: storage account + container Pipeline CopyActivity Executa a cópia com parallelCopies=4 e DIU=4 Storage Account Conta de armazenamento Azure que recebe os dados Container Namespace dentro da Storage Account (equivale a um bucket) Como as Partes se Conectam O ADF age como intermediário inteligente: ele autentica no S3 usando as credenciais do IAM, lê os objetos (recursivamente, se configurado), e grava em paralelo no Blob Storage usando a connection string da Storage Account. A pipeline usa formato Binary em ambos os lados - isso preserva os arquivos como estão, sem transformação, o que é exatamente o que queremos em uma migração de dados brutos.
O Ansible não move nenhum dado diretamente. Ele é responsável por orquestrar o provisionamento de todos os recursos no az CLI e, ao final, fazer polling do runId da pipeline até receber Succeeded ou Failed.
Estrutura do Projeto ansible-adf-migrate-s3-azure/ ├── playbook.yml # playbook principal - tudo em um só arquivo ├── vars.yml # variáveis não-sensíveis (pode commitar) ├── vars_vault.yml # credenciais AWS - criptografar com ansible-vault └── diagrama/ ├── gerar_gif.py ├── ansible-adf-migrate-s3-azure.gif └── ansible-adf-migrate-s3-azure.html Pré-requisitos Antes de executar, você precisa:
az CLI instalado e autenticado (az login) Ansible instalado (pip install ansible) IAM user na AWS com permissões: s3:GetObject s3:GetObjectVersion ansible-vault para criptografar as credenciais AWS Preparando as Variáveis vars.yml - configure com os valores do seu ambiente:
resource_group: &#34;rg-migrate-s3-azure&#34; location: &#34;eastus&#34; storage_account_name: &#34;stdadosmigrados&#34; container_name: &#34;dados-migrados&#34; adf_name: &#34;adf-migrate-s3&#34; adf_pipeline_name: &#34;pipeline-s3-to-blob&#34; s3_bucket_name: &#34;meu-bucket-s3&#34; s3_region: &#34;us-east-1&#34; s3_prefix: &#34;&#34; # vazio = copia tudo; ou &#34;pasta/&#34; para subdiretório log_level: verbose vars_vault.yml - nunca commitar sem criptografia:
aws_access_key_id: &#34;AKIAIOSFODNN7EXAMPLE&#34; aws_secret_access_key: &#34;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&#34; Criptografar antes de qualquer commit:
ansible-vault encrypt vars_vault.yml Playbook - Explicação por Bloco O playbook está dividido em 9 tasks principais mais pre_tasks e post_tasks.
pre_tasks - Verificação de Ambiente pre_tasks: - name: Verificar az CLI instalado command: az --version changed_when: false failed_when: az_check.rc != 0 - name: Verificar login na Azure command: az account show changed_when: false failed_when: az_account.rc != 0 Falha imediatamente se o az CLI não estiver instalado ou se não houver sessão ativa. Economiza tempo debugando tasks que viriam depois.
Tasks 1-2 - Resource Group e Storage Account - name: &#34;[ 1/9 ] Criar Resource Group&#34; command: &gt; az group create --name {{ resource_group }} --location {{ location }} --output none - name: &#34;[ 2/9 ] Criar Storage Account&#34; command: &gt; az storage account create --name {{ storage_account_name }} --resource-group {{ resource_group }} --sku Standard_LRS --kind StorageV2 --access-tier Hot --output none - name: &#34;[ 2/9 ] Criar container com acesso privado&#34; command: &gt; az storage container create --name {{ container_name }} --account-name {{ storage_account_name }} --public-access off --auth-mode login --output none O container é criado com public-access off - o acesso é controlado pelo ADF via connection string, não por URL pública. Isso é diferente de quando você quer servir arquivos via CDN (nesse caso, --public-access blob).
Tasks 3-5 - ADF e Linked Services - name: &#34;[ 3/9 ] Criar Azure Data Factory&#34; command: &gt; az datafactory factory create --factory-name {{ adf_name }} --resource-group {{ resource_group }} --location {{ location }} - name: &#34;[ 4/9 ] Criar Linked Service - AWS S3&#34; command: &gt; az datafactory linked-service create --factory-name {{ adf_name }} --linked-service-name {{ adf_linked_s3_name }} --properties &#39;{ &#34;type&#34;: &#34;AmazonS3&#34;, &#34;typeProperties&#34;: { &#34;accessKeyId&#34;: &#34;{{ aws_access_key_id }}&#34;, &#34;secretAccessKey&#34;: { &#34;type&#34;: &#34;SecureString&#34;, &#34;value&#34;: &#34;{{ aws_secret_access_key }}&#34; }, &#34;authenticationType&#34;: &#34;AccessKey&#34; } }&#39; no_log: true # nunca logar credenciais O no_log: true garante que a task não apareça em logs de auditoria com as credenciais expostas. Sempre usar em tasks que passam segredos como argumento de CLI.
Tasks 6-7 - Datasets e Pipeline Os datasets definem o &ldquo;onde&rdquo; da cópia: o S3 como AmazonS3Location com bucketName e folderPath, e o Blob como AzureBlobStorageLocation com container. Ambos usam tipo Binary - sem transformação de dados.
A pipeline usa parallelCopies: 4 e dataIntegrationUnits: 4, que é o mínimo para extrair performance razoável em objetos médios. Para migrações de terabytes, aumentar para 32 DIUs.
Tasks 8-9 - Execução e Polling - name: &#34;[ 8/9 ] Executar pipeline&#34; command: &gt; az datafactory pipeline create-run --factory-name {{ adf_name }} --name {{ adf_pipeline_name }} register: pipeline_run - name: &#34;[ 9/9 ] Aguardar conclusão (polling 30s)&#34; command: &gt; az datafactory pipeline-run show --run-id {{ run_id }} --query status --output tsv until: pipeline_status.stdout.strip() in [&#39;Succeeded&#39;, &#39;Failed&#39;, &#39;Cancelled&#39;] retries: 120 delay: 30 O until com retries: 120 e delay: 30 cobre até 1 hora de execução. Para buckets muito grandes, aumentar retries.
Executando # 1. criptografar credenciais AWS ansible-vault encrypt vars_vault.yml # 2. executar o playbook ansible-playbook playbook.yml -e @vars.yml --ask-vault-pass # 3. verificar o resultado no portal # https://adf.azure.com Para testar sem criar recursos (dry-run não funciona com az CLI, mas você pode checar a sintaxe YAML):
ansible-playbook playbook.yml --syntax-check Após a migração, verificar os blobs via CLI:
az storage blob list \ --container-name dados-migrados \ --account-name stdadosmigrados \ --auth-mode login \ --query &#34;[].name&#34; \ --output table Monitoramento e Troubleshooting Sintoma Causa provável Solução AuthorizationFailure no linked service S3 IAM sem s3:GetObject Adicionar a policy no IAM user da AWS StorageErrorCode: AuthorizationFailure no Blob az CLI sem role no storage Atribuir Storage Blob Data Contributor ao usuário Pipeline travada em InProgress DIU insuficiente para o volume Aumentar dataIntegrationUnits na pipeline Pipeline terminated with status: Failed Erro na cópia de objeto específico Verificar logs no ADF Monitor: https://adf.azure.com az: command not found az CLI não instalado curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash retries exceeded no polling Pipeline demorou mais de 1h Aumentar retries no playbook Para Que Serve no Mercado Migrações de cloud são comuns em contextos de aquisição de empresa, mudança de provedor, ou consolidação de infraestrutura. Ter um playbook parametrizado para esse fluxo permite que o time de DevOps ou SRE replique a operação para N buckets com mínima intervenção - só ajusta s3_bucket_name, container_name e roda.
O ADF também é usado para sincronização contínua com triggers agendados, não só migração one-shot. O mesmo playbook pode ser adaptado para provisionar um trigger no ADF que executa a pipeline periodicamente.
Conclusão A combinação Ansible + ADF é mais robusta do que scripts de shell com aws s3 sync + azcopy porque o ADF gerencia o estado da cópia, faz retry automático em falhas transitórias e dá visibilidade detalhada no portal. O Ansible garante que toda a infraestrutura é declarativa, versionada e reproduzível - rodar duas vezes não cria recursos duplicados.
A migração real que deu origem a este lab copiou 10GB em aproximadamente 9 minutos com configuração padrão de 4 DIUs.
Referências Azure Data Factory - documentação oficial Copiar dados do Amazon S3 para o Azure Blob Storage az datafactory - referência CLI Ansible command module Azure Blob Storage - documentação Playbook Completo --- # ============================================================================= # Migração S3 → Azure Blob Storage via Azure Data Factory # Playbook: playbook.yml # Autor: fxshell # Uso: ansible-playbook playbook.yml -e @vars.yml --ask-vault-pass # ============================================================================= - name: Migração S3 → Azure Blob Storage via ADF hosts: localhost connection: local gather_facts: false vars_files: - vars_vault.yml # arquivo criptografado com ansible-vault vars: resource_group: &#34;rg-migrate-s3-azure&#34; location: &#34;eastus&#34; storage_account_name: &#34;stdadosmigrados&#34; container_name: &#34;dados-migrados&#34; adf_name: &#34;adf-migrate-s3&#34; adf_pipeline_name: &#34;pipeline-s3-to-blob&#34; adf_linked_s3_name: &#34;LinkedService_S3&#34; adf_linked_blob_name: &#34;LinkedService_BlobStorage&#34; adf_dataset_s3_name: &#34;Dataset_S3&#34; adf_dataset_blob_name: &#34;Dataset_Blob&#34; s3_bucket_name: &#34;meu-bucket-s3&#34; s3_region: &#34;us-east-1&#34; # Prefixo de objetos a migrar (vazio = tudo) s3_prefix: &#34;&#34; # Nível de log: verbose | normal log_level: normal # --------------------------------------------------------------------------- # PRÉ-REQUISITOS: verificar dependências antes de qualquer task # --------------------------------------------------------------------------- pre_tasks: - name: Verificar az CLI instalado command: az --version register: az_check changed_when: false failed_when: az_check.rc != 0 - name: Verificar login na Azure (az account show) command: az account show register: az_account changed_when: false failed_when: az_account.rc != 0 - name: Exibir subscription ativa debug: msg: &#34;Subscription ativa: {{ (az_account.stdout | from_json).name }}&#34; when: log_level == &#34;verbose&#34; # --------------------------------------------------------------------------- # BLOCO 1 - Resource Group # --------------------------------------------------------------------------- tasks: - name: &#34;[ 1/9 ] Criar Resource Group {{ resource_group }}&#34; command: &gt; az group create --name {{ resource_group }} --location {{ location }} --output none register: rg_result changed_when: &#39;&#34;Succeeded&#34; in rg_result.stdout or rg_result.rc == 0&#39; # --------------------------------------------------------------------------- # BLOCO 2 - Storage Account + Container # --------------------------------------------------------------------------- - name: &#34;[ 2/9 ] Criar Storage Account {{ storage_account_name }}&#34; command: &gt; az storage account create --name {{ storage_account_name }} --resource-group {{ resource_group }} --location {{ location }} --sku Standard_LRS --kind StorageV2 --access-tier Hot --output none register: sa_result changed_when: sa_result.rc == 0 - name: &#34;[ 2/9 ] Obter connection string do Storage Account&#34; command: &gt; az storage account show-connection-string --name {{ storage_account_name }} --resource-group {{ resource_group }} --query connectionString --output tsv register: sa_conn_string changed_when: false no_log: true - name: &#34;[ 2/9 ] Criar container &#39;{{ container_name }}&#39; com acesso privado&#34; command: &gt; az storage container create --name {{ container_name }} --account-name {{ storage_account_name }} --public-access off --auth-mode login --output none register: container_result changed_when: container_result.rc == 0 # --------------------------------------------------------------------------- # BLOCO 3 - Azure Data Factory # --------------------------------------------------------------------------- - name: &#34;[ 3/9 ] Criar Azure Data Factory {{ adf_name }}&#34; command: &gt; az datafactory factory create --factory-name {{ adf_name }} --resource-group {{ resource_group }} --location {{ location }} --output none register: adf_result changed_when: adf_result.rc == 0 - name: &#34;[ 3/9 ] Obter ID do Storage Account para ADF&#34; command: &gt; az storage account show --name {{ storage_account_name }} --resource-group {{ resource_group }} --query id --output tsv register: sa_id changed_when: false # --------------------------------------------------------------------------- # BLOCO 4 - Linked Service: AWS S3 # ADF precisa de access key + secret key com permissões s3:GetObject # e s3:GetObjectVersion no bucket de origem # --------------------------------------------------------------------------- - name: &#34;[ 4/9 ] Criar Linked Service - AWS S3&#34; command: &gt; az datafactory linked-service create --factory-name {{ adf_name }} --resource-group {{ resource_group }} --linked-service-name {{ adf_linked_s3_name }} --properties &#39;{ &#34;type&#34;: &#34;AmazonS3&#34;, &#34;typeProperties&#34;: { &#34;serviceUrl&#34;: &#34;https://s3.amazonaws.com&#34;, &#34;accessKeyId&#34;: &#34;{{ aws_access_key_id }}&#34;, &#34;secretAccessKey&#34;: { &#34;type&#34;: &#34;SecureString&#34;, &#34;value&#34;: &#34;{{ aws_secret_access_key }}&#34; }, &#34;authenticationType&#34;: &#34;AccessKey&#34; } }&#39; register: ls_s3_result changed_when: ls_s3_result.rc == 0 no_log: true # --------------------------------------------------------------------------- # BLOCO 5 - Linked Service: Azure Blob Storage # --------------------------------------------------------------------------- - name: &#34;[ 5/9 ] Criar Linked Service - Azure Blob Storage&#34; command: &gt; az datafactory linked-service create --factory-name {{ adf_name }} --resource-group {{ resource_group }} --linked-service-name {{ adf_linked_blob_name }} --properties &#39;{ &#34;type&#34;: &#34;AzureBlobStorage&#34;, &#34;typeProperties&#34;: { &#34;connectionString&#34;: &#34;{{ sa_conn_string.stdout | trim }}&#34; } }&#39; register: ls_blob_result changed_when: ls_blob_result.rc == 0 no_log: true # --------------------------------------------------------------------------- # BLOCO 6 - Datasets (origem S3 + destino Blob) # --------------------------------------------------------------------------- - name: &#34;[ 6/9 ] Criar Dataset origem - S3 Binary&#34; command: &gt; az datafactory dataset create --factory-name {{ adf_name }} --resource-group {{ resource_group }} --dataset-name {{ adf_dataset_s3_name }} --properties &#39;{ &#34;type&#34;: &#34;Binary&#34;, &#34;linkedServiceName&#34;: { &#34;referenceName&#34;: &#34;{{ adf_linked_s3_name }}&#34;, &#34;type&#34;: &#34;LinkedServiceReference&#34; }, &#34;typeProperties&#34;: { &#34;location&#34;: { &#34;type&#34;: &#34;AmazonS3Location&#34;, &#34;bucketName&#34;: &#34;{{ s3_bucket_name }}&#34;, &#34;folderPath&#34;: &#34;{{ s3_prefix }}&#34; } } }&#39; register: ds_s3_result changed_when: ds_s3_result.rc == 0 - name: &#34;[ 6/9 ] Criar Dataset destino - Blob Binary&#34; command: &gt; az datafactory dataset create --factory-name {{ adf_name }} --resource-group {{ resource_group }} --dataset-name {{ adf_dataset_blob_name }} --properties &#39;{ &#34;type&#34;: &#34;Binary&#34;, &#34;linkedServiceName&#34;: { &#34;referenceName&#34;: &#34;{{ adf_linked_blob_name }}&#34;, &#34;type&#34;: &#34;LinkedServiceReference&#34; }, &#34;typeProperties&#34;: { &#34;location&#34;: { &#34;type&#34;: &#34;AzureBlobStorageLocation&#34;, &#34;container&#34;: &#34;{{ container_name }}&#34; } } }&#39; register: ds_blob_result changed_when: ds_blob_result.rc == 0 # --------------------------------------------------------------------------- # BLOCO 7 - Pipeline de cópia S3 → Blob # --------------------------------------------------------------------------- - name: &#34;[ 7/9 ] Criar Pipeline de migração {{ adf_pipeline_name }}&#34; command: &gt; az datafactory pipeline create --factory-name {{ adf_name }} --resource-group {{ resource_group }} --name {{ adf_pipeline_name }} --pipeline &#39;{ &#34;activities&#34;: [ { &#34;name&#34;: &#34;CopyS3ToBlob&#34;, &#34;type&#34;: &#34;Copy&#34;, &#34;inputs&#34;: [ { &#34;referenceName&#34;: &#34;{{ adf_dataset_s3_name }}&#34;, &#34;type&#34;: &#34;DatasetReference&#34; } ], &#34;outputs&#34;: [ { &#34;referenceName&#34;: &#34;{{ adf_dataset_blob_name }}&#34;, &#34;type&#34;: &#34;DatasetReference&#34; } ], &#34;typeProperties&#34;: { &#34;source&#34;: { &#34;type&#34;: &#34;BinarySource&#34;, &#34;storeSettings&#34;: { &#34;type&#34;: &#34;AmazonS3ReadSettings&#34;, &#34;recursive&#34;: true } }, &#34;sink&#34;: { &#34;type&#34;: &#34;BinarySink&#34;, &#34;storeSettings&#34;: { &#34;type&#34;: &#34;AzureBlobStorageWriteSettings&#34; } }, &#34;parallelCopies&#34;: 4, &#34;dataIntegrationUnits&#34;: 4, &#34;enableStaging&#34;: false } } ] }&#39; register: pipeline_result changed_when: pipeline_result.rc == 0 # --------------------------------------------------------------------------- # BLOCO 8 - Disparar execução da pipeline # --------------------------------------------------------------------------- - name: &#34;[ 8/9 ] Executar pipeline de migração&#34; command: &gt; az datafactory pipeline create-run --factory-name {{ adf_name }} --resource-group {{ resource_group }} --name {{ adf_pipeline_name }} register: pipeline_run changed_when: pipeline_run.rc == 0 - name: &#34;[ 8/9 ] Capturar Run ID&#34; set_fact: run_id: &#34;{{ (pipeline_run.stdout | from_json).runId }}&#34; - name: &#34;[ 8/9 ] Exibir Run ID&#34; debug: msg: &#34;Pipeline iniciada - Run ID: {{ run_id }}&#34; # --------------------------------------------------------------------------- # BLOCO 9 - Aguardar conclusão e verificar status # Polling a cada 30s, timeout de 120 tentativas (≈ 1h) # --------------------------------------------------------------------------- - name: &#34;[ 9/9 ] Aguardar conclusão da pipeline (polling 30s)&#34; command: &gt; az datafactory pipeline-run show --factory-name {{ adf_name }} --resource-group {{ resource_group }} --run-id {{ run_id }} --query status --output tsv register: pipeline_status until: pipeline_status.stdout.strip() in [&#39;Succeeded&#39;, &#39;Failed&#39;, &#39;Cancelled&#39;] retries: 120 delay: 30 changed_when: false - name: &#34;[ 9/9 ] Falhar se a pipeline não completou com sucesso&#34; fail: msg: &gt; Pipeline terminou com status: {{ pipeline_status.stdout.strip() }}. Acesse https://adf.azure.com para detalhes do erro. when: pipeline_status.stdout.strip() != &#39;Succeeded&#39; - name: &#34;[ 9/9 ] Exibir resultado final&#34; debug: msg: | ============================================================ Migração concluída com sucesso! Origem : s3://{{ s3_bucket_name }}/{{ s3_prefix }} Destino : {{ storage_account_name }}/{{ container_name }} Run ID : {{ run_id }} ADF URL : https://adf.azure.com/en-us/home?factory=%2FresourceGroups%2F{{ resource_group }}%2Fproviders%2FMicrosoft.DataFactory%2Ffactories%2F{{ adf_name }} ============================================================ # --------------------------------------------------------------------------- # PÓS-TASKS: limpeza opcional de recursos temporários # --------------------------------------------------------------------------- post_tasks: - name: &#34;[ CLEANUP ] Listar blobs migrados para validação&#34; command: &gt; az storage blob list --container-name {{ container_name }} --account-name {{ storage_account_name }} --auth-mode login --query &#34;[].name&#34; --output table register: blob_list changed_when: false when: log_level == &#34;verbose&#34; - name: &#34;[ CLEANUP ] Exibir blobs migrados&#34; debug: msg: &#34;{{ blob_list.stdout }}&#34; when: log_level == &#34;verbose&#34; and blob_list is defined ]]></content:encoded>
    </item>
    <item>
      <title>Application Gateway no Azure com Terraform — Lab de Backend API com Path Routing</title>
      <link>https://fxshell.com.br/posts/terraform-azure-appgw/</link>
      <pubDate>Sat, 25 Apr 2026 00:00:00 UT</pubDate>
      <dc:creator>Felipe da Matta</dc:creator>
      <guid>https://fxshell.com.br/posts/terraform-azure-appgw/</guid>
      <description>Se a sua aplicação expõe múltiplas APIs numa mesma infraestrutura e você precisa rotear o tráfego por URL — /api/orders para um pool, /api/inventory para outro — um Load Balancer tradicional não resolve. Ele opera na camada 4 (TCP/UDP), enxerga apenas IP e porta, e distribui conexões sem entender o conteúdo HTTP. O Azure Application Gateway opera na camada 7 e faz exatamente esse roteamento baseado em path, header e host — além de oferecer SSL termination, health probes HTTP e integração com WAF.
</description>
      <content:encoded><![CDATA[Se a sua aplicação expõe múltiplas APIs numa mesma infraestrutura e você precisa rotear o tráfego por URL — /api/orders para um pool, /api/inventory para outro — um Load Balancer tradicional não resolve. Ele opera na camada 4 (TCP/UDP), enxerga apenas IP e porta, e distribui conexões sem entender o conteúdo HTTP. O Azure Application Gateway opera na camada 7 e faz exatamente esse roteamento baseado em path, header e host — além de oferecer SSL termination, health probes HTTP e integração com WAF.
Este lab provisiona com Terraform um cenário completo: uma API backend em Python rodando em 3 VMs, um Application Gateway com URL path map, health probes customizados e backend pools separados por rota.
Application Gateway vs Load Balancer — O Que Muda Antes de entrar no lab, vale entender por que escolher um em vez do outro. A diferença fundamental é a camada OSI em que cada um opera:
Característica Azure Load Balancer (L4) Azure Application Gateway (L7) Camada OSI 4 — TCP/UDP 7 — HTTP/HTTPS O que enxerga IP de origem/destino, porta URL, headers, cookies, hostname Roteamento Por IP + porta (regras de NAT) Por URL path, header, hostname Health probe TCP ou HTTP (código de status) HTTP com path, hostname e match de body SSL/TLS Pass-through (não termina) SSL termination e re-encryption WAF Não Sim (WAF v2 integrado ou add-on) Session affinity Por IP de origem (hash) Cookie-based affinity nativo Rewrite Não Headers, URL path e query string Autoscale Sim Sim (Standard_v2) Custo Mais barato Mais caro (paga por capacity units) Caso de uso Tráfego não-HTTP (DB, DNS, gaming) APIs, microsserviços, apps web Quando usar Load Balancer Tráfego TCP/UDP puro — bancos de dados, servidores DNS, jogos multiplayer Cenários onde você só precisa distribuir conexões por IP e porta Custo é prioridade e você não precisa de nenhum recurso HTTP Balanceamento interno entre VMs na mesma VNet sem exposição pública Quando usar Application Gateway APIs HTTP/HTTPS que precisam de roteamento por URL path (microsserviços) Aplicações que exigem SSL termination centralizado Cenários que precisam de WAF para proteção contra OWASP Top 10 Session affinity por cookie (aplicações stateful) Redirect HTTP → HTTPS, rewrite de headers e URL Vantagens do Application Gateway em detalhes Roteamento por URL path — O recurso central. Com url_path_map, o gateway inspeciona o path da requisição HTTP e direciona para backend pools diferentes. /api/orders pode ir para um conjunto de servidores otimizado para consultas de pedidos, enquanto /api/inventory vai para outro pool com mais memória. Com Load Balancer, todo tráfego na porta 80/443 vai para o mesmo backend — não há como diferenciar por rota.
Health probes HTTP inteligentes — O Load Balancer verifica se a porta TCP está respondendo. O Application Gateway faz um GET /health (ou qualquer path configurado), valida o status code HTTP e pode até verificar o conteúdo do body. Se o backend retorna 200 mas com {&quot;status&quot;:&quot;degraded&quot;}, o probe pode detectar isso. Isso reduz drasticamente falsos positivos de &ldquo;healthy&rdquo; em backends que aceitam conexão TCP mas não estão funcionando corretamente.
SSL termination (offload) — O TLS é terminado no gateway, que faz a descriptografia e encaminha tráfego HTTP puro para os backends. Isso elimina o custo de CPU de TLS em cada VM backend e centraliza a gestão de certificados em um único ponto. Opcionalmente, o gateway pode re-encriptar o tráfego para o backend (end-to-end TLS), mas o certificado pode ser diferente — self-signed, por exemplo.
WAF integrado — O Application Gateway com SKU WAF_v2 inclui proteção contra ataques OWASP Top 10 (SQL injection, XSS, CSRF) sem precisar de um appliance separado. As regras são gerenciadas centralmente e atualizadas pela Microsoft. Com Load Balancer, proteção WAF exige uma solução externa.
Autoscale zone-redundant (v2) — O SKU Standard_v2 escala automaticamente as instâncias do gateway baseado no tráfego e pode ser distribuído entre Availability Zones, eliminando o gateway como ponto único de falha.
Objetivo do Lab Provisionar com Terraform um Application Gateway que recebe requisições HTTP, roteia por URL path para backend pools distintos e monitora a saúde dos backends com health probes HTTP customizados. A aplicação backend é uma API Python minimalista que serve /api/orders, /api/inventory e /health.
Tecnologias Utilizadas Terraform é a ferramenta de Infrastructure as Code que provisiona todos os recursos no Azure de forma declarativa e versionável.
Azure Application Gateway é o balanceador de carga de camada 7 da Microsoft que inspeciona o conteúdo HTTP das requisições e roteia para diferentes backend pools baseado em regras de URL path, headers e hostnames.
Azure Virtual Network isola a infraestrutura com subnets dedicadas — uma para o Application Gateway (requisito obrigatório) e outra para os backends.
Cloud-init configura as VMs no primeiro boot, instalando Python e iniciando a API backend como um serviço systemd, sem necessidade de acesso SSH manual.
Arquitetura ┌──────────────────────────────────────────────────────────────────────┐ │ REQUEST FLOW │ │ │ │ ┌───────────┐ HTTPS ┌────────────────────────────┐ │ │ │ Usuario │ ────────► │ Application Gateway │ │ │ │ (Internet) │ │ Standard_v2 │ │ │ └───────────┘ │ │ │ │ │ Listener :80 │ │ │ │ URL Path Map: │ │ │ │ /api/orders → pool-orders │ │ │ │ /api/inventory → pool-inv │ │ │ │ Health Probe: GET /health │ │ │ └──────────┬───────────────────┘ │ │ │ :8080 │ │ ┌──────────┼──────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │backend-1 │ │backend-2 │ │backend-3 │ │ │ │10.10.2.4 │ │10.10.2.5 │ │10.10.2.6 │ │ │ │ Python │ │ Python │ │ Python │ │ │ │ API:8080 │ │ API:8080 │ │ API:8080 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────────────────────────────────────────────────┘ Componente Função Application Gateway Recebe tráfego HTTP, roteia por URL path, monitora health dos backends Listener Escuta na porta 80 com o IP público do gateway URL Path Map Mapeia /api/orders e /api/inventory para backend pools diferentes Health Probe Verifica /health a cada 15s — remove backend do pool após 3 falhas Backend Pool Grupo de VMs que recebe o tráfego roteado pelo gateway vm-backend-1..3 VMs Ubuntu com API Python servindo na porta 8080 Como Funciona o Path-Based Routing Quando uma requisição chega ao Application Gateway, o processamento segue esta sequência:
O Listener aceita a conexão HTTP na porta 80 (ou 443 com TLS) A Request Routing Rule associa o listener ao URL Path Map O URL Path Map inspeciona o path da URL e encontra a path_rule correspondente A requisição é encaminhada para o Backend Pool definido na regra, usando as configurações do Backend HTTP Settings (porta 8080, timeout, affinity) O gateway mantém um Health Probe periódico que faz GET /health em cada backend — backends que falham são removidos do pool automaticamente Se nenhuma path_rule corresponde ao path da requisição, o gateway usa o default_backend_address_pool definido no path map.
Subnet dedicada: O Application Gateway v2 exige uma subnet dedicada (sem outras VMs ou serviços). O Terraform cria snet-appgw exclusivamente para o gateway e snet-backend para as VMs — não tente colocar ambos na mesma subnet.
Estrutura do Projeto terraform-azure-appgw/ ├── main.tf # Provider, backend, módulos ├── variables.tf # Variáveis de entrada ├── outputs.tf # Outputs (IP público, FQDN) ├── modules/ │ ├── networking/ │ │ ├── main.tf # VNet, subnets, NSGs │ │ ├── variables.tf │ │ └── outputs.tf │ ├── backend_app/ │ │ ├── main.tf # VMs + cloud-init (API Python) │ │ ├── variables.tf │ │ └── outputs.tf │ └── app_gateway/ │ ├── main.tf # AppGW, listeners, path map, probes │ ├── variables.tf │ └── outputs.tf └── diagrama/ ├── terraform-azure-appgw.html ├── terraform-azure-appgw.gif └── gerar_gif.py Networking — VNet e Subnets A VNet usa o CIDR 10.10.0.0/16 com duas subnets isoladas:
10.10.0.0/16 (vnet-appgw-lab) ├── 10.10.1.0/24 snet-appgw ← Application Gateway (dedicada) └── 10.10.2.0/24 snet-backend ← VMs backend O NSG da subnet do gateway permite HTTP (80), HTTPS (443) e o range de portas de gerenciamento 65200-65535 (obrigatório para o Application Gateway v2 funcionar — sem essa regra, o health do gateway fica degraded).
security_rule { name = &#34;AllowGatewayManager&#34; priority = 120 direction = &#34;Inbound&#34; access = &#34;Allow&#34; protocol = &#34;Tcp&#34; source_port_range = &#34;*&#34; destination_port_range = &#34;65200-65535&#34; source_address_prefix = &#34;GatewayManager&#34; destination_address_prefix = &#34;*&#34; } O NSG da subnet de backend permite tráfego apenas da subnet do gateway na porta 8080 — nenhum acesso direto da internet aos backends:
security_rule { name = &#34;AllowAppGwToBackend&#34; priority = 100 direction = &#34;Inbound&#34; access = &#34;Allow&#34; protocol = &#34;Tcp&#34; source_port_range = &#34;*&#34; destination_port_range = &#34;8080&#34; source_address_prefix = &#34;10.10.1.0/24&#34; destination_address_prefix = &#34;*&#34; } Backend API — Python com cloud-init Cada VM backend roda uma API HTTP em Python puro (sem framework) na porta 8080, provisionada automaticamente via cloud-init no primeiro boot. A API serve três endpoints:
Endpoint Retorno GET /health {&quot;status&quot;: &quot;healthy&quot;} — usado pelo health probe GET /api/orders Lista de pedidos fictícios + hostname do servidor GET /api/inventory Lista de estoque fictício + hostname do servidor O hostname no response permite verificar qual backend respondeu — útil para confirmar que o round-robin está funcionando.
class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == &#34;/health&#34;: self.send_response(200) self.send_header(&#34;Content-Type&#34;, &#34;application/json&#34;) self.end_headers() self.wfile.write(json.dumps({&#34;status&#34;: &#34;healthy&#34;}).encode()) elif self.path == &#34;/api/orders&#34;: data = { &#34;server&#34;: socket.gethostname(), &#34;orders&#34;: [ {&#34;id&#34;: 1, &#34;product&#34;: &#34;Widget A&#34;, &#34;qty&#34;: 10}, {&#34;id&#34;: 2, &#34;product&#34;: &#34;Widget B&#34;, &#34;qty&#34;: 5}, ] } # ... A API roda como serviço systemd (api-backend.service) com Restart=always, garantindo que ela reinicie automaticamente em caso de crash.
Application Gateway — Path Map e Health Probes O gateway é o componente central. O Terraform configura:
URL Path Map url_path_map { name = &#34;url-path-map&#34; default_backend_address_pool_name = &#34;pool-orders&#34; default_backend_http_settings_name = &#34;http-settings-default&#34; path_rule { name = &#34;rule-orders&#34; paths = [&#34;/api/orders&#34;, &#34;/api/orders/*&#34;] backend_address_pool_name = &#34;pool-orders&#34; backend_http_settings_name = &#34;http-settings-default&#34; } path_rule { name = &#34;rule-inventory&#34; paths = [&#34;/api/inventory&#34;, &#34;/api/inventory/*&#34;] backend_address_pool_name = &#34;pool-inventory&#34; backend_http_settings_name = &#34;http-settings-default&#34; } } O default_backend_address_pool captura qualquer path que não tenha regra explícita — sem ele, o gateway retorna 502.
Health Probe probe { name = &#34;probe-health&#34; host = &#34;127.0.0.1&#34; path = &#34;/health&#34; protocol = &#34;Http&#34; interval = 15 timeout = 10 unhealthy_threshold = 3 } O probe faz GET /health a cada 15 segundos. Se um backend não responder 200 em 3 tentativas consecutivas (45 segundos), ele é removido do pool. Quando volta a responder, é readicionado automaticamente.
Backend HTTP Settings backend_http_settings { name = &#34;http-settings-default&#34; cookie_based_affinity = &#34;Disabled&#34; port = 8080 protocol = &#34;Http&#34; request_timeout = 30 probe_name = &#34;probe-health&#34; } O cookie_based_affinity está desabilitado porque a API é stateless — cada requisição pode ir para qualquer backend. Em aplicações stateful (sessões), habilitar isso faz o gateway inserir um cookie ApplicationGatewayAffinity que fixa o usuário no mesmo backend.
Executando # Inicializar terraform init # Revisar o plano terraform plan -var=&#34;admin_password=SenhaSegura123!&#34; # Aplicar terraform apply -var=&#34;admin_password=SenhaSegura123!&#34; # Testar os endpoints (após ~5 min para provisionamento + cloud-init) curl http://$(terraform output -raw appgw_public_ip)/api/orders curl http://$(terraform output -raw appgw_public_ip)/api/inventory curl http://$(terraform output -raw appgw_public_ip)/health Executando múltiplas requisições, o campo server no JSON alterna entre vm-backend-1, vm-backend-2 e vm-backend-3 — confirmando que o round-robin distribui o tráfego entre os backends.
# Verificar distribuição round-robin for i in $(seq 1 10); do curl -s http://$(terraform output -raw appgw_public_ip)/api/orders | jq .server done Monitoramento e Troubleshooting Verificar health dos backends # Via Azure CLI — mostra o estado de cada backend no pool az network application-gateway show-backend-health \ --resource-group rg-appgw-lab \ --name appgw-lab \ --query &#39;backendAddressPools[].backendHttpSettingsCollection[].servers[]&#39; \ --output table Logs de diagnóstico O Application Gateway gera logs de acesso e performance que podem ser enviados para um Log Analytics Workspace:
# Habilitar diagnostic settings az monitor diagnostic-settings create \ --name appgw-diagnostics \ --resource $(az network application-gateway show -g rg-appgw-lab -n appgw-lab --query id -o tsv) \ --workspace $(az monitor log-analytics workspace show -g rg-appgw-lab -n law-appgw-lab --query id -o tsv) \ --logs &#39;[{&#34;category&#34;:&#34;ApplicationGatewayAccessLog&#34;,&#34;enabled&#34;:true},{&#34;category&#34;:&#34;ApplicationGatewayPerformanceLog&#34;,&#34;enabled&#34;:true}]&#39; Problemas comuns Sintoma Causa provável Solução Gateway health: Unhealthy Falta da regra NSG para portas 65200-65535 Adicionar regra AllowGatewayManager Backend: Unhealthy Health probe falhando (API não está rodando) Verificar systemctl status api-backend na VM HTTP 502 Nenhum backend healthy no pool Verificar NSG da subnet backend e porta 8080 HTTP 504 Backend demora mais que o request_timeout Aumentar timeout no backend_http_settings Para Que Serve no Mercado O Application Gateway é o ponto de entrada padrão para aplicações web e APIs no Azure. Em arquiteturas de microsserviços, ele substitui a necessidade de um reverse proxy (Nginx/HAProxy) gerenciado manualmente, centralizando roteamento, TLS e WAF em um serviço gerenciado com autoscale.
A capacidade de rotear por URL path permite que uma única URL pública (api.empresa.com) distribua tráfego para dezenas de microsserviços diferentes, cada um em seu backend pool — sem que o cliente precise saber quantos serviços existem por trás.
Para times que já usam Terraform, o Application Gateway é um recurso complexo de configurar pela primeira vez (listeners, rules, probes, pools — tudo é interdependente), mas uma vez modularizado como neste lab, fica trivial replicar para novos ambientes.
Conclusão O Application Gateway resolve um problema que o Load Balancer não consegue: rotear tráfego HTTP com inteligência. Se a sua aplicação é web ou API, o gateway oferece roteamento por path, health probes inteligentes, SSL termination e WAF — tudo em um único serviço gerenciado. O Load Balancer continua sendo a escolha certa para tráfego não-HTTP (bancos de dados, protocolos customizados), mas para qualquer coisa que fala HTTP, o Application Gateway é a ferramenta adequada.
Referências Azure Application Gateway — Documentação oficial URL path-based routing overview Application Gateway vs Load Balancer Health probes — Application Gateway WAF on Application Gateway Terraform azurerm_application_gateway Subnet requirements for Application Gateway v2 ]]></content:encoded>
    </item>
    <item>
      <title>Automatizando Backups de SQL Server no Azure com Ansible</title>
      <link>https://fxshell.com.br/posts/ansible-mssql-backup-azure/</link>
      <pubDate>Tue, 21 Apr 2026 00:00:00 UT</pubDate>
      <dc:creator>Felipe da Matta</dc:creator>
      <guid>https://fxshell.com.br/posts/ansible-mssql-backup-azure/</guid>
      <description>Manter backups consistentes de múltiplos SQL Servers e armazená-los em nuvem é uma tarefa crítica que, quando feita manualmente, vira fonte de erros, esquecimentos e surpresas na hora de um restore. Este lab mostra como resolver isso com Ansible rodando em uma máquina Linux, acionando servidores Windows via WinRM e fazendo o backup ir direto do SQL Server para o Azure Blob Storage — sem que nenhum arquivo passe pela máquina de controle.
</description>
      <content:encoded><![CDATA[Manter backups consistentes de múltiplos SQL Servers e armazená-los em nuvem é uma tarefa crítica que, quando feita manualmente, vira fonte de erros, esquecimentos e surpresas na hora de um restore. Este lab mostra como resolver isso com Ansible rodando em uma máquina Linux, acionando servidores Windows via WinRM e fazendo o backup ir direto do SQL Server para o Azure Blob Storage — sem que nenhum arquivo passe pela máquina de controle.
Objetivo do Lab Construir uma automação completa que, a partir de uma máquina Linux, conecta em múltiplos SQL Servers Windows via WinRM, cria um SQL Server CREDENTIAL com o SAS Token do Azure, e dispara os três tipos de backup nativos do SQL Server (Full, Differential e Transaction Log) diretamente para o Azure Blob Storage via BACKUP TO URL. O projeto inclui também um playbook de restore com suporte a ponto no tempo (point-in-time recovery), onde o SQL Server lê os arquivos diretamente do Azure via RESTORE FROM URL.
Tecnologias Utilizadas Ansible é a ferramenta de automação de infraestrutura que orquestra todo o fluxo. Ela executa tarefas em hosts remotos sem precisar instalar agentes — usa SSH para Linux e WinRM para Windows. No mercado é amplamente usada por times DevOps e SRE para padronizar operações repetitivas.
WinRM (Windows Remote Management) é o protocolo da Microsoft equivalente ao SSH para Windows. O Ansible usa ele, com autenticação NTLM, para executar comandos em servidores Windows sem precisar de nenhum agente instalado.
sqlcmd é a ferramenta de linha de comando do SQL Server para executar scripts T-SQL. O Ansible gera o script .sql via template Jinja2 e chama o sqlcmd remotamente para executá-lo.
BACKUP TO URL / RESTORE FROM URL é o mecanismo nativo do SQL Server (disponível desde 2012 SP1) para fazer backup e restore diretamente de e para o Azure Blob Storage, sem staging local. O SQL Server autentica via um objeto CREDENTIAL que contém o SAS Token — nenhum dado transita pela máquina Ansible.
SQL Server CREDENTIAL é um objeto de segurança do SQL Server que armazena a identidade e o segredo de autenticação para um recurso externo. Neste projeto, o CREDENTIAL aponta para a URL base da conta de armazenamento e usa o SAS Token como SECRET. O Ansible cria ou atualiza esse CREDENTIAL antes de cada execução de backup.
ansible-vault é o mecanismo de criptografia do Ansible para proteger variáveis sensíveis — senhas, tokens, chaves — dentro dos arquivos de inventário e variáveis do projeto.
Arquitetura ┌─────────────────────────────────────────────────────────────────────┐ │ BACKUP FLOW │ │ │ │ ┌──────────────┐ WinRM/NTLM ┌──────────┐ ┌──────────┐ │ │ │ ops-linux │ ─────────────► │ sql01 │ │ sql02 │ │ │ │ (trigger) │ │ sql03 │ │ ... │ │ │ └──────────────┘ └────┬─────┘ └────┬─────┘ │ │ │ BACKUP TO URL│ │ │ ▼ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ Azure Blob Storage │ │ │ │ 📦 sql-backup-full │ │ │ │ 📦 sql-backup-diff │ │ │ │ 📦 sql-backup-log │ │ │ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────┐ │ RESTORE FLOW │ │ │ │ ┌──────────────┐ lista blobs ┌──────────────────────────────┐ │ │ │ ops-linux │ ─────────────► │ Azure Blob Storage │ │ │ │ (orquestra) │ ◄─ URLs ───── │ 📦 containers (full/diff/log│ │ │ └──────┬───────┘ └──────────────────────────────┘ │ │ │ WinRM + URLs ▲ │ │ ▼ │ RESTORE FROM URL │ │ ┌──────────────────┐ ──────────────────────────┘ │ │ │ sql-target │ │ │ │ (RESTORE) │ │ │ └──────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ Componente Função ops-linux Ansible Controller — dispara o backup via WinRM e orquestra o restore (somente metadados) sql01..03 SQL Servers Windows — executam BACKUP TO URL e RESTORE FROM URL diretamente sqlcmd Executa os scripts T-SQL de backup/restore no SQL Server SQL CREDENTIAL Objeto no SQL Server que autentica no Azure via SAS Token Azure Blob Storage Destino e origem dos backups, separados por containers Como Funciona o Backup e Restore Direto Com BACKUP TO URL e RESTORE FROM URL, o SQL Server abre uma conexão TLS direto para o endpoint do Azure Blob Storage e faz o stream do backup sem criar arquivo intermediário em nenhum ponto do caminho. A máquina ops envia apenas o script T-SQL (alguns KB) via WinRM e aguarda a conclusão. Nenhum byte de dados de backup trafega pela máquina de controle — ela funciona exclusivamente como orquestradora.
Atenção ao horário: o stream TLS direto do SQL Server para o Azure consome banda de rede da máquina de banco de dados. Em backups Full de bancos grandes (centenas de GB), o upload pode saturar a interface de rede e impactar a latência de queries em produção. Recomenda-se agendar backups Full e o restore na madrugada — janela de menor carga — e reservar o horário comercial para backups Differential e Log, que são muito menores.
Estrutura do Projeto ansible-mssql-backup-azure/ ├── ansible.cfg ├── inventory/ │ └── hosts.ini ├── group_vars/ │ ├── all.yml # Azure storage + SAS Token (vault) │ └── sqlservers.yml # credenciais SQL + lista de bancos ├── playbook-backup.yml ├── playbook-restore.yml └── roles/ ├── mssql_backup/ │ ├── tasks/ │ │ ├── main.yml # cria CREDENTIAL + direciona por tipo │ │ ├── full.yml │ │ ├── diff.yml │ │ └── log.yml │ └── templates/ │ ├── backup_full.sql.j2 │ ├── backup_diff.sql.j2 │ └── backup_log.sql.j2 └── mssql_restore/ ├── tasks/ │ ├── main.yml # cria CREDENTIAL + chama restore.yml │ └── restore.yml └── templates/ └── restore.sql.j2 Inventário e Variáveis O inventário declara os SQL Servers (Windows, via WinRM). A máquina ops não precisa mais estar no inventário para os backups:
[sqlservers] sql01 ansible_host=192.168.1.20 sql02 ansible_host=192.168.1.21 sql03 ansible_host=192.168.1.22 [sqlservers:vars] ansible_user=ansible_svc ansible_password=&#34;{{ vault_winrm_password }}&#34; ansible_connection=winrm ansible_winrm_transport=ntlm ansible_winrm_server_cert_validation=ignore ansible_port=5985 Em group_vars/sqlservers.yml, cada banco é declarado com seu modelo de recuperação, o que determina se backup de log é possível:
databases: - name: &#34;AppDB&#34; recovery_model: &#34;FULL&#34; - name: &#34;LegacyDB&#34; recovery_model: &#34;SIMPLE&#34; # só aceita full e diff backup_compression: true verify_backup: true Em group_vars/all.yml, a URL base da conta é usada tanto para construir as URLs dos blobs quanto como nome do CREDENTIAL no SQL Server:
azure_storage_account: &#34;minhaconta&#34; azure_container_full: &#34;sql-backup-full&#34; azure_container_diff: &#34;sql-backup-diff&#34; azure_container_log: &#34;sql-backup-log&#34; # SAS Token: permissões read + write + create + list vault_azure_sas_token: !vault | $ANSIBLE_VAULT;1.1;AES256 &lt;criptografado&gt; azure_blob_base_url: &#34;https://{{ azure_storage_account }}.blob.core.windows.net&#34; Para criar o vault:
ansible-vault encrypt_string &#39;sv=2022-11-02&amp;ss=b&amp;...&#39; --name &#39;vault_azure_sas_token&#39; SQL Server CREDENTIAL Antes de executar qualquer backup, a role cria (ou atualiza) um CREDENTIAL no SQL Server com o SAS Token. Esse objeto é o que permite o SQL Server autenticar no Azure sem precisar de credenciais da conta de armazenamento:
CREATE CREDENTIAL [https://minhaconta.blob.core.windows.net] WITH IDENTITY = &#39;SHARED ACCESS SIGNATURE&#39;, SECRET = &#39;sv=2022-11-02&amp;ss=b&amp;srt=co&amp;sp=rwdlc&amp;se=2027-01-01...&amp;sig=...&#39;; O Ansible executa esse script via win_shell + sqlcmd com no_log: true, garantindo que o SAS Token nunca apareça nos logs da execução.
Os Três Tipos de Backup Full — Backup Completo O backup Full captura o banco inteiro. É sempre o ponto de partida para qualquer cadeia de restore.
ansible-playbook playbook-backup.yml -e backup_type=full --ask-vault-pass O script T-SQL gerado pelo template aponta direto para a URL do blob no Azure:
BACKUP DATABASE [AppDB] TO URL = N&#39;https://minhaconta.blob.core.windows.net/sql-backup-full/sql01/2026-04-24/sql01_AppDB_FULL_20260424030000.bak&#39; WITH FORMAT, INIT, NAME = N&#39;AppDB - Full Backup 20260424030000&#39;, COMPRESSION, STATS = 10, CHECKSUM; Após o backup, o SQL Server executa RESTORE VERIFYONLY FROM URL para confirmar a integridade do arquivo diretamente no Azure, antes do Ansible registrar o resultado.
Differential — Backup Diferencial Captura apenas os dados que mudaram desde o último Full. Muito mais rápido e menor em tamanho, ideal para execuções diárias quando o Full é semanal.
ansible-playbook playbook-backup.yml -e backup_type=diff --ask-vault-pass BACKUP DATABASE [AppDB] TO URL = N&#39;https://minhaconta.blob.core.windows.net/sql-backup-diff/sql01/2026-04-24/sql01_AppDB_DIFF_20260424120000.bak&#39; WITH DIFFERENTIAL, COMPRESSION, STATS = 10, CHECKSUM; Transaction Log — Backup de Log de Transações Captura todas as transações registradas no log desde o último backup de log. Permite restaurar o banco para qualquer ponto no tempo (point-in-time recovery). Requer que o banco use o modelo de recuperação FULL ou BULK_LOGGED — o script verifica isso antes de executar.
ansible-playbook playbook-backup.yml -e backup_type=log --ask-vault-pass BACKUP LOG [AppDB] TO URL = N&#39;https://minhaconta.blob.core.windows.net/sql-backup-log/sql01/2026-04-24/sql01_AppDB_LOG_20260424150000.bak&#39; WITH COMPRESSION, STATS = 10, CHECKSUM; Fluxo Interno de Cada Backup Para cada banco, a role mssql_backup executa na sequência:
Cria ou atualiza o SQL Server CREDENTIAL com o SAS Token (via win_shell + sqlcmd, no_log: true) Monta a URL de destino: {conta}/{container}/{host}/{data}/{host}_{banco}_{tipo}_{timestamp}.bak Gera o script .sql via win_template (Jinja2) e salva em C:\Windows\Temp\ Executa o backup com sqlcmd — o SQL Server abre conexão TLS direto para o Azure e faz o stream Verifica integridade com RESTORE VERIFYONLY FROM URL (quando verify_backup: true) Remove o script .sql temporário do SQL Server A máquina ops envia apenas texto (script SQL + comandos WinRM). Nenhum byte de dados de backup trafega por ela.
Playbook de Restore O restore suporta quatro modos:
Modo O que aplica full Somente backup Full full_diff Full + Differential mais recente full_diff_log Full + Diff + todos os Logs em sequência point_in_time Full + Diff + Logs até um STOPAT específico O playbook lista os blobs disponíveis no Azure via API REST (apenas metadados, sem download) usando o módulo uri do Ansible, e passa as URLs para o SQL Server executar RESTORE FROM URL:
# Restore simples ansible-playbook playbook-restore.yml \ -e restore_db=AppDB \ -e restore_target_host=sql01 \ -e restore_date=2026-04-24 \ --ask-vault-pass # Point-in-time recovery ansible-playbook playbook-restore.yml \ -e restore_db=AppDB \ -e restore_target_host=sql01 \ -e restore_date=2026-04-24 \ -e restore_mode=point_in_time \ -e restore_stopat=&#34;2026-04-24T14:30:00&#34; \ --ask-vault-pass # Restaurar com nome diferente (não sobrescreve o banco original) ansible-playbook playbook-restore.yml \ -e restore_db=AppDB \ -e restore_new_name=AppDB_RestoreTest \ -e restore_target_host=sql01 \ -e restore_date=2026-04-24 \ -e restore_mode=full_diff \ --ask-vault-pass O script T-SQL de restore lê os arquivos diretamente do Azure, gerenciando o estado do banco durante a sequência:
-- FULL com NORECOVERY (banco fica em &#34;restaurando&#34; para aceitar diff/log) RESTORE DATABASE [AppDB] FROM URL = N&#39;https://minhaconta.blob.core.windows.net/sql-backup-full/sql01/2026-04-24/sql01_AppDB_FULL_20260424030000.bak&#39; WITH NORECOVERY, REPLACE, STATS = 10; -- DIFF com NORECOVERY RESTORE DATABASE [AppDB] FROM URL = N&#39;https://minhaconta.blob.core.windows.net/sql-backup-diff/sql01/2026-04-24/sql01_AppDB_DIFF_20260424120000.bak&#39; WITH NORECOVERY, STATS = 10; -- LOG final com RECOVERY (encerra a sequência) e STOPAT opcional RESTORE LOG [AppDB] FROM URL = N&#39;https://minhaconta.blob.core.windows.net/sql-backup-log/sql01/2026-04-24/sql01_AppDB_LOG_20260424150000.bak&#39; WITH STOPAT = &#39;2026-04-24 14:30:00&#39;, RECOVERY; -- Volta para multi-user ALTER DATABASE [AppDB] SET MULTI_USER; O uso de NORECOVERY em todos os passos exceto o último é obrigatório — ele mantém o banco em modo de restauração para aceitar os próximos arquivos. Apenas o último comando usa RECOVERY, que finaliza a sequência e coloca o banco online.
Segurança Todos os segredos ficam no vault — nunca em texto plano:
# Criptografar SAS token do Azure (permissões: read + write + create + list) ansible-vault encrypt_string &#39;sv=2022-11-02&amp;ss=b...&#39; --name &#39;vault_azure_sas_token&#39; # Criptografar senha WinRM ansible-vault encrypt_string &#39;SenhaDoServico123!&#39; --name &#39;vault_winrm_password&#39; # Criptografar senha SQL ansible-vault encrypt_string &#39;SenhaSql123!&#39; --name &#39;mssql_password&#39; O SAS Token é passado para o SQL Server dentro do script T-SQL gerado em C:\Windows\Temp\ com no_log: true no Ansible, e o arquivo temporário é removido imediatamente após a execução. O token nunca aparece nos logs do Ansible nem no histórico do shell.
O usuário SQL usado (backup_svc) precisa das permissões mínimas para BACKUP TO URL:
CREATE LOGIN backup_svc WITH PASSWORD = &#39;SenhaSql123!&#39;; GRANT CONNECT SQL TO backup_svc; -- Em cada banco: EXEC sp_addrolemember &#39;db_backupoperator&#39;, &#39;backup_svc&#39;; GRANT VIEW DATABASE STATE TO backup_svc; -- Para criar o CREDENTIAL (necessário para BACKUP TO URL): GRANT ALTER ANY CREDENTIAL TO backup_svc; Executando # Testar conectividade antes de tudo ansible sqlservers -m win_ping --ask-vault-pass # Backup Full de todos os bancos (recomendado: madrugada — ex: 02:00) ansible-playbook playbook-backup.yml -e backup_type=full --ask-vault-pass # Backup Full de um banco específico ansible-playbook playbook-backup.yml -e backup_type=full -e target_db=AppDB --ask-vault-pass # Backup Differential (pode rodar durante o dia — arquivo menor, menos impacto de rede) ansible-playbook playbook-backup.yml -e backup_type=diff --ask-vault-pass # Backup de Log (pode rodar a cada hora — arquivo pequeno, impacto mínimo) ansible-playbook playbook-backup.yml -e backup_type=log --ask-vault-pass # Restore full + diff + log (recomendado: madrugada — evita concorrência com queries ativas) ansible-playbook playbook-restore.yml \ -e restore_db=AppDB \ -e restore_target_host=sql01 \ -e restore_date=2026-04-24 \ -e restore_mode=full_diff_log \ --ask-vault-pass Agendamento sugerido (cron no controller) # Full todo domingo às 02:00 0 2 * * 0 cd /opt/ansible/mssql-backup &amp;&amp; ansible-playbook playbook-backup.yml -e backup_type=full --vault-password-file=.vault_pass # Differential de segunda a sábado às 02:00 0 2 * * 1-6 cd /opt/ansible/mssql-backup &amp;&amp; ansible-playbook playbook-backup.yml -e backup_type=diff --vault-password-file=.vault_pass # Transaction Log a cada hora (bancos com recovery_model FULL) 0 * * * * cd /opt/ansible/mssql-backup &amp;&amp; ansible-playbook playbook-backup.yml -e backup_type=log --vault-password-file=.vault_pass Lifecycle Management — Reduzindo Custo de Armazenamento O BACKUP TO URL grava os arquivos no tier Hot por padrão. Isso faz sentido nos primeiros dias, quando a chance de precisar de um restore rápido é maior. Mas backups são arquivos que você escreve uma vez e só lê em emergência — manter meses de backups no tier Hot é jogar dinheiro fora.
O Azure Blob Storage oferece quatro tiers de armazenamento, cada um com custo e latência de acesso diferentes:
Tier Custo/GB/mês (aprox.) Acesso Uso ideal Hot ~$0.018 Imediato Backups recentes (últimos 7 dias) Cool ~$0.010 Imediato Backups da última semana a um mês Cold ~$0.0036 Imediato Backups de 1 a 3 meses Archive ~$0.002 Horas para reidratar Retenção longa ou compliance A diferença é significativa: um backup de 100 GB no tier Hot custa ~$1.80/mês, no Cold custa ~$0.36/mês. Em um ambiente com vários servidores e meses de retenção, a economia acumula rápido.
Lifecycle Management Policy Em vez de mover blobs manualmente entre tiers, o Azure permite criar uma Lifecycle Management Policy no Storage Account. Essa política é avaliada automaticamente uma vez por dia e move os blobs entre tiers com base na idade — sem script, sem cron, sem custo de operação.
A estratégia recomendada para backups:
0-7 dias → Hot (restore imediato se precisar) 7-30 dias → Cool (metade do custo) 30-90 dias → Cold (1/5 do custo) 90+ dias → Archive ou deletar (conforme retenção exigida) Configurando via Azure CLI Primeiro, crie um arquivo JSON com a política de lifecycle. Este exemplo aplica a movimentação progressiva entre tiers e deleta backups com mais de 365 dias:
{ &#34;rules&#34;: [ { &#34;enabled&#34;: true, &#34;name&#34;: &#34;backup-lifecycle&#34;, &#34;type&#34;: &#34;Lifecycle&#34;, &#34;definition&#34;: { &#34;actions&#34;: { &#34;baseBlob&#34;: { &#34;tierToCool&#34;: { &#34;daysAfterModificationGreaterThan&#34;: 7 }, &#34;tierToCold&#34;: { &#34;daysAfterModificationGreaterThan&#34;: 30 }, &#34;tierToArchive&#34;: { &#34;daysAfterModificationGreaterThan&#34;: 90 }, &#34;delete&#34;: { &#34;daysAfterModificationGreaterThan&#34;: 365 } } }, &#34;filters&#34;: { &#34;blobTypes&#34;: [&#34;blockBlob&#34;], &#34;prefixMatch&#34;: [ &#34;sql-backup-full/&#34;, &#34;sql-backup-diff/&#34;, &#34;sql-backup-log/&#34; ] } } } ] } O filtro prefixMatch garante que a política se aplica apenas aos containers de backup, sem afetar outros blobs na mesma Storage Account.
Agora aplique a política na Storage Account:
# Aplicar a lifecycle policy az storage account management-policy create \ --account-name minhaconta \ --resource-group meu-rg \ --policy @lifecycle-policy.json # Verificar a política aplicada az storage account management-policy show \ --account-name minhaconta \ --resource-group meu-rg Considerações sobre Archive e Restore O tier Archive tem o menor custo de armazenamento, mas a reidratação leva horas (Standard: até 15 horas, High Priority: até 1 hora com custo maior). Se o restore precisa ser rápido, considere usar Cold como tier final em vez de Archive — o custo é ligeiramente maior, mas o acesso é imediato.
Para verificar em qual tier cada blob está:
# Listar blobs com o tier de acesso az storage blob list \ --account-name minhaconta \ --container-name sql-backup-full \ --query &#34;[].{name:name, tier:properties.blobTier, modified:properties.lastModified}&#34; \ --output table \ --auth-mode login A Lifecycle Policy é avaliada uma vez por dia pelo Azure. Após criar a política, os blobs existentes serão movidos gradualmente nas próximas 24-48 horas conforme as regras definidas.
Para Que Serve no Mercado Times de DBA e SRE que gerenciam ambientes com SQL Server Windows enfrentam o desafio de manter backups consistentes sem depender de jobs do SQL Server Agent configurados manualmente em cada instância. Com Ansible, a política de backup fica no código, versionada no Git, aplicável a qualquer número de servidores com um único comando.
O modelo BACKUP TO URL faz o SQL Server enviar o backup diretamente ao Azure via TLS, sem staging intermediário. A máquina de controle funciona apenas como orquestradora — sem impacto de I/O de dados, independente do tamanho dos backups. Isso permite escalar o número de servidores e bancos sem aumentar disco na infraestrutura Ansible.
O suporte a point-in-time recovery é o que diferencia um backup operacional de um backup de compliance — em caso de corrupção de dados, ransomware ou erro humano, a capacidade de restaurar para um momento específico pode ser a diferença entre minutos e horas de downtime.
Conclusão Automatizar backups não é apenas uma questão de conveniência — é uma prática de resiliência. Quando o restore precisa acontecer, não é hora de descobrir que o backup estava corrompido, desatualizado ou mal documentado. Este projeto aplica verificação de integridade no próprio Azure após cada backup (RESTORE VERIFYONLY FROM URL), cadeia de restore estruturada no código, segredos protegidos por vault e zero impacto de disco na máquina de controle — tornando o processo auditável, reproduzível e escalável.
Referências Documentação do Ansible para Windows BACKUP TO URL — SQL Server para Microsoft Azure Criar um SQL Server Credential para autenticação no Azure T-SQL BACKUP DATABASE T-SQL RESTORE DATABASE ansible-vault Lifecycle Management Policy — Azure Blob Storage Access Tiers — Hot, Cool, Cold, Archive ]]></content:encoded>
    </item>
  </channel>
</rss>
