">
 

CI/CD for Azure Logic Apps Standard on a Private (ILB) ASE — Without a VNet Agent

Iniciado por joomlamz, Hoje at 06:25

Respostas: 1   |   Visualizações: 4

Tópico anterior - Tópico seguinte

0 Membros e 1 Visitante estão a ver este tópico.

Saudações, comunidade do **webmastersmz.com**! Como especialista em tecnologia, analisei o tópico em inglês sobre **CI/CD para Azure Logic Apps Standard num ASE Privado (ILB) sem um Agente VNet**, e trago-vos os pontos altos desta discussão técnica.

### Análise Técnica: CI/CD em Logic Apps Standard com ILB ASE (Sem Agente VNet)

Implementar pipelines de CI/CD para Logic Apps Standard num Ambiente de Serviço de Aplicações (ASE) com Balanceador de Carga Interno (ILB), e ainda por cima sem recorrer a um agente dedicado na VNet, traz desafios arquitetónicos fascinantes, mas totalmente viáveis. Os pontos principais que merecem destaque são:

1. **Isolamento de Rede e Acesso ao ILB ASE:** Como o ILB ASE não tem endereços IP públicos, o agente de CI/CD (seja Azure DevOps Pipelines ou GitHub Actions) precisa de encontrar uma forma de comunicar com o ASE para fazer o *deploy* dos artefactos. Sem um agente dentro da VNet, a solução passa frequentemente por utilizar mecanismos como *Self-hosted agents* em VMs estratégicas, ou configurar corretamente as regras de firewall e VPN/ExpressRoute.
2. **Empacotamento e Estrutura do Projeto:** As Logic Apps Standard baseiam-se no runtime do Azure Functions. Isto significa que o processo de build deve compilar corretamente as dependências e gerar um pacote `.zip` estruturado com os fluxos de trabalho (`workflow.json`), conexões e configurações locais.
3. **Gestão de Conexões e Definições de Aplicação:** Um dos maiores escolhos em ambientes altamente seguros é garantir que as ligações geridas (*managed connectors*) e as configurações de ligação API (`connections.json`) funcionam após o *deploy* no ASE privado, uma vez que as credenciais e as identidades geridas (*Managed Identities*) devem ser rigorosamente mapeadas para o ambiente de produção.
4. **Deploy via API/Kudu (SCM):** Sem um agente na VNet, o pipeline normalmente recorre aos pontos finais de SCM (Kudu) do ASE, desde que haja visibilidade de rede através de um túnel seguro ou de um *jump host*, permitindo o envio do pacote de implementação via REST API ou tarefas nativas do Azure CLI.

Esta abordagem é essencial para empresas em Moçambique e no mundo que exigem os mais altos níveis de conformidade e segurança, mantendo a agilidade do desenvolvimento moderno através de DevOps.

### Vamos ao debate!
Como é que vocês têm lidado com a implementação de arquiteturas híbridas e seguras no Azure aqui na nossa comunidade? Já se depararam com barreiras de rede ao configurar pipelines para ambientes isolados (ASE)? Deixem as vossas experiências, dúvidas ou abordagens alternativas aqui nos comentários do **webmastersmz.com** para enriquecermos este conhecimento técnico!

---

Para garantir que os vossos projetos, aplicações e fóruns rodam sem falhas, com alta disponibilidade e segurança, convido-vos a conhecer as soluções de alojamento de alta performance da **AplicHost** em [https://aplichost.com](https://aplichost.com).

CI/CD for Azure Logic Apps Standard on a Private (ILB) ASE — Without a VNet Agent



Tópico: CI/CD for Azure Logic Apps Standard on a Private (ILB) ASE — Without a VNet Agent
Categoria: Tutoriais | Programação & Tecnologia
Idioma Principal: Português (Conteúdo de Tecnologia)

Descrição do Conteúdo / Informações:
-------------------------------------------------------------------------
How we wired an Azure DevOps pipeline to a Logic App Standard running on an internal App Service Environment, hit six real-world failures on the way, and ended with a deployment that needs no SAS tokens, no SCM access, and no VNet-connected build agent.



The problem


Logic Apps Standard on an ILB App Service Environment (ASE v3) has no public SCM (Kudu) endpoint. That kills the two "normal" deployment routes:

Route
Why it fails on ILB ASE

az webapp deploy / zip-deploy from a Microsoft-hosted agent
Talks to the SCM endpoint, which is private

VS Code / portal publish
Same SCM dependency, plus it isn't CI/CD

The usual answer is "stand up a self-hosted agent inside the VNet." That's the right long-term answer — but you can ship today without it, using Run-From-Package with a managed-identity-authenticated blob URL. The pipeline only ever talks to two public planes:


Azure Storage (upload the package)


ARM / management.azure.com (set one app setting, restart the app)

The app then pulls the package itself, from inside the VNet, using its own managed identity. SCM is never touched. No SAS token ever exists.

Git push ──▶ ADO pipeline ──▶ zip ──▶ Blob Storage

│ (pull via app's
│  managed identity)
ARM: set app setting ──▶ Logic App (ILB ASE)
ARM: restart app



What you need before starting


#
Prerequisite
Notes

1
Logic App Standard resource, Started

A stopped app silently accepts settings and deploys nothing. Check the Overview blade — ours had been sitting in Stopped and cost us a confused hour

2
A storage account the ASE can reach
Same region as the ASE. Public network access enabled is the simple path; a private endpoint works too

3
A blob container, private access
e.g. deployments

4
An ADO agent (hosted or self-hosted) with Azure CLI installed

Ours was a Windows self-hosted box that did not have az — see failure #3

5
An ARM service connection
Any auth type works. If it's the "Managed identity (agent-assigned)" type, the identity must actually be enabled on the agent VM — see failure #2

6
Rights to create role assignments on the storage account and the Logic App's resource group
Or a friendly admin



Step 1 — Repository layout


One folder per Logic App Standard resource; one subfolder per workflow. The zip we deploy is simply the app folder's contents.

logicapps-platform/
├── apps/
│   └── la-integration-prod/          ← 1 folder = 1 Logic App Standard resource
│       ├── host.json                 ← runtime + extension bundle
│       ├── connections.json          ← connector references ({} to start)
│       ├── parameters.json           ← per-environment values ({} to start)
│       ├── .funcignore
│       └── Heartbeat-Test/           ← 1 folder = 1 workflow
│           └── workflow.json
└── pipelines/
└── deploy-la-integration-prod.yml

host.json:

{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
"version": "[1.*, 2.0.0)"
}
}

connections.json:

{
"managedApiConnections": {},
"serviceProviderConnections": {}
}

Heartbeat-Test/workflow.json — a deliberately trivial pilot workflow. No connectors, no secrets, nothing that can fail for reasons unrelated to the pipeline. Prove the pipe first; migrate real workflows second.

{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"Every_Hour": {
"type": "Recurrence",
"recurrence": { "frequency": "Hour", "interval": 1 }
}
},
"actions": {
"Compose_Heartbeat": {
"type": "Compose",
"inputs": {
"message": "Deployed via ADO pipeline",
"runAt": "@{utcNow()}",
"workflow": "@{workflow().name}"
},
"runAfter": {}
}
},
"outputs": {}
},
"kind": "Stateful"
}

Path gotcha: if your files live in a subfolder of the repo (e.g. myrepo/logicapps-platform/apps/...), the pipeline's sourceFolder variable and trigger paths must include that prefix. Our first run failed with Cannot find path ...\apps\ for exactly this reason.



Step 2 — Identity and role assignments


Three grants total. Two for the pipeline's identity (whatever your service connection authenticates as), one for the Logic App's own identity.



2a. Pipeline identity


Role
Scope
Purpose

Storage Blob Data Contributor
The storage account
Upload the package

Website Contributor
The Logic App's resource group
Set app settings + restart

az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee-object-id <pipeline-identity-object-id> \
--assignee-principal-type ServicePrincipal \
--scope "/subscriptions/<sub-id>/resourceGroups/<storage-rg>/providers/Microsoft.Storage/storageAccounts/<storage-account>"

az role assignment create \
--role "Website Contributor" \
--assignee-object-id <pipeline-identity-object-id> \
--assignee-principal-type ServicePrincipal \
--scope "/subscriptions/<sub-id>/resourceGroups/<logicapp-rg>"

How to find the pipeline identity's object ID when nobody remembers it: run a throwaway pipeline on the target pool that asks the instance metadata service directly:

trigger: none
pool:
name: 'SelfHosted-Windows-Pool'
steps:
- powershell: |
$r = Invoke-RestMethod -Headers @{Metadata="true"} -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
$p = $r.access_token.Split('.')[1].Replace('-','+').Replace('_','/')
switch ($p.Length % 4) { 2 {$p+='=='}; 3 {$p+='='} }
$c = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($p)) | ConvertFrom-Json
Write-Host "objectId : $($c.oid)"
Write-Host "resource : $($c.xms_mirid)"   # full ARM ID of the VM - tells you where it lives
displayName: Who am I

If this returns Identity not found, the agent VM's system-assigned identity is off — enable it (VM → Identity → System assigned → On) and note the object ID it produces. This exact thing happened to us: the MI-type service connection had existed for months and had never worked.



2b. Logic App identity (the key to SAS-free deployment)


Enable the app's system-assigned identity (Logic App → Identity → System assigned → On), then:

az role assignment create \
--role "Storage Blob Data Reader" \
--assignee-object-id <logicapp-identity-object-id> \
--assignee-principal-type ServicePrincipal \
--scope "/subscriptions/<sub-id>/resourceGroups/<storage-rg>/providers/Microsoft.Storage/storageAccounts/<storage-account>"

RBAC propagation is real. Allow ~5 minutes between creating a role assignment and testing it. Restarting the app 30 seconds after the grant gives a false failure.



Step 3 — The pipeline


The complete, working YAML. Written for a Windows agent (all steps PowerShell); on a Linux agent, switch scriptType: ps to bash and adjust the validation step.

# ============================================================================
# Deploy Logic App Standard via Run-From-Package (Blob, MI-authenticated URL)
# The app pulls the package with ITS OWN managed identity: no SAS, no expiry,
# and the SCM endpoint is never touched, so an ILB ASE is not a problem.
#
# PREREQS:
#   - Logic App system-assigned identity ON, with 'Storage Blob Data Reader'
#     on the storage account
#   - Pipeline identity: 'Storage Blob Data Contributor' on the storage
#     account and 'Website Contributor' on the Logic App's resource group
# ============================================================================

trigger:
branches:
include: [ main ]
paths:
include:
- apps/la-integration-prod/**
- pipelines/**

pool:
name: 'SelfHosted-Windows-Pool'     # or vmImage: 'ubuntu-latest' for SP-auth connections

variables:
serviceConnection: 'my-azure-service-connection'
resourceGroup:     'rg-logicapps-prod'
appName:           'la-integration-prod'
storageAccount:    'stdeploypkgs'
container:         'deployments'
sourceFolder:      'apps/la-integration-prod'
packageName:       'la-integration-prod-$(Build.BuildId).zip'

stages:

# ----------------------------------------------------------------------------
- stage: Build
displayName: Validate & package
jobs:
- job: Package
steps:

- powershell: |
$bad = 0
Get-ChildItem -Path "$(sourceFolder)" -Recurse -Filter *.json | ForEach-Object {
$raw = Get-Content $_.FullName -Raw
try { $raw | ConvertFrom-Json | Out-Null }
catch { Write-Host "##vso[task.logissue type=error]Invalid JSON: $($_.FullName)"; $bad++ }
if ($raw -match '"(secret|client_secret|clientSecret|password)"\s*:\s*"(?!@)') {
Write-Host "##vso[task.logissue type=error]Inline secret in: $($_.FullName)"; $bad++
}
}
if ($bad -gt 0) { exit 1 }
displayName: Validate JSON + block inline secrets

- task: ArchiveFiles@2
displayName: Zip app content
inputs:
rootFolderOrFile: '$(sourceFolder)'
includeRootFolder: false          # host.json must sit at the ZIP ROOT
archiveType: zip
archiveFile: '$(Build.ArtifactStagingDirectory)/$(packageName)'

- publish: '$(Build.ArtifactStagingDirectory)/$(packageName)'
artifact: package

# ----------------------------------------------------------------------------
- stage: Deploy
displayName: Deploy to $(appName)
dependsOn: Build
jobs:
- deployment: Deploy
environment: 'logicapps-prod'         # add an approval gate on this environment
strategy:
runOnce:
deploy:
steps:

- download: current
artifact: package

- task: AzureCLI@2
displayName: Upload package to blob
inputs:
azureSubscription: '$(serviceConnection)'
scriptType: ps
scriptLocation: inlineScript
inlineScript: |
az storage blob upload `
--account-name $(storageAccount) `
--container-name $(container) `
--name $(packageName) `
--file "$(Pipeline.Workspace)/package/$(packageName)" `
--auth-mode login --overwrite

- task: AzureCLI@2
displayName: Point app at package (MI-auth URL, no SAS, no SCM)
inputs:
azureSubscription: '$(serviceConnection)'
scriptType: ps
scriptLocation: inlineScript
inlineScript: |
$ErrorActionPreference = "Stop"
$url = "https://$(storageAccount).blob.core.windows.net/$(container)/$(packageName)"

az webapp config appsettings set `
-g "$(resourceGroup)" -n "$(appName)" `
--settings WEBSITE_RUN_FROM_PACKAGE="$url" WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID="SystemAssigned" `
-o none

az webapp restart -g "$(resourceGroup)" -n "$(appName)"
Write-Host "Deployed $(packageName). App restarted."

- task: AzureCLI@2
displayName: Verify workflow exists
inputs:
azureSubscription: '$(serviceConnection)'
scriptType: ps
scriptLocation: inlineScript
inlineScript: |
Start-Sleep -Seconds 60
$sub = az account show --query id -o tsv
az rest --method GET `
--uri "https://management.azure.com/subscriptions/$sub/resourceGroups/$(resourceGroup)/providers/Microsoft.Web/sites/$(appName)/workflows?api-version=2022-03-01" `
--query "value[].name" -o tsv

Three lines carry the whole trick:


WEBSITE_RUN_FROM_PACKAGE = the plain blob URL — no SAS query string.


WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID=SystemAssigned — tells App Service to fetch the package using the app's own managed identity. Without this setting the runtime attempts anonymous access, fails, and the app reports ServiceUnavailable from the host runtime. This is the single most-missed step.


includeRootFolder: false in the archive task — host.json must be at the root of the zip, not nested one level down.



Step 4 — Run it, and what "success" looks like


• Create the pipeline from the YAML, run it. First run will pause to ask permission on the service connection / environment / pool — click Permit.

• Green pipeline, and the Verify step prints your workflow name(s).

• Portal → Logic App → Workflows shows the workflow, Stateful, Enabled.

• Open it → Run history → confirm a run actually executed with the expected output. A workflow that loads but never runs is not success.



Everything that actually went wrong (the useful part)


Our path to green, in order. If you're debugging, scan this table first.

#
Symptom
Root cause
Fix

1
Unable to locate executable file: 'bash'
Assumed Linux; the self-hosted agent was Windows

All steps → PowerShell (scriptType: ps)

2

Invoke-RestMethod : {"error":"invalid_request","error_description":"Identity not found"} on the metadata endpoint
Agent VM's system-assigned managed identity was off — the MI-type service connection had never actually worked
VM → Identity → System assigned → On. If the identity was enabled after boot, a VM restart may be needed before the token endpoint responds

3
Azure CLI 2.x is not installed on this machine
Fresh agent VM, no az

Install via portal Run Command (no RDP needed), then restart the agent service so it picks up the new PATH:
Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile C:\az.msi
Start-Process msiexec.exe -ArgumentList '/i C:\az.msi /quiet /norestart' -Wait
`Get-Service vstsagent* \

4
{% raw %}Get-ChildItem : Cannot find path '...\apps\'

Repo files nested one folder deeper than the YAML expected
Fix sourceFolder and trigger paths to include the prefix

5
ERROR: incorrect usage: --expiry should be within 7 days from now
Identity-based (user-delegation) SAS is capped at 7 days — and a 7-day SAS would be a time bomb anyway, since the app re-reads the package URL on every restart
Drop SAS entirely; switch to the MI-authenticated plain URL (this article's approach)

6
Pipeline green, but portal shows Error retrieving workflows. Encountered an error (ServiceUnavailable) from host runtime

App couldn't pull the package: WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID missing, and the app's identity had no role on storage
Add the app setting; grant Storage Blob Data Reader to the app's identity; wait ~5 min for RBAC; restart



A 60-second diagnostic when the runtime won't start


Run from any machine with az — checks the four usual suspects at once:

$rg="rg-logicapps-prod"; $app="la-integration-prod"
$st="stdeploypkgs"; $strg="rg-storage"; $sub="<sub-id>"

# 1. Identity on?
az webapp identity show -g $rg -n $app --query principalId -o tsv

# 2. Both settings present?
az webapp config appsettings list -g $rg -n $app `
--query "[?starts_with(name,'WEBSITE_RUN_FROM')].{n:name,v:value}" -o table

# 3. App identity has a role on storage?
az role assignment list --assignee <principalId-from-step-1> `
--scope "/subscriptions/$sub/resourceGroups/$strg/providers/Microsoft.Storage/storageAccounts/$st" `
--query "[].roleDefinitionName" -o tsv

# 4. Storage reachable at all?
az storage account show -g $strg -n $st `
--query "{publicNetworkAccess:publicNetworkAccess, defaultAction:networkRuleSet.defaultAction}" -o table

Whichever check comes back empty is your culprit.



Why this beats the alternatives


Approach
Verdict

SAS URL in WEBSITE_RUN_FROM_PACKAGE

Works, but user-delegation SAS caps at 7 days and account-key SAS means handling keys. Either way the app dies quietly when the token expires — on whatever future day it happens to restart

Zip-deploy via VNet self-hosted agent
The classic answer. Fine — but it makes deployment hostage to one VM's health, and you still shouldn't need SCM for a package-based app

MI-authenticated Run-From-Package
No secrets, no expiry, no SCM, works from any agent that can reach ARM + Storage. The package is also immutable per build ID, so rollback = point the setting at the previous zip and restart



Hardening checklist for after the pilot


• Approval gate on the ADO Environment before the Deploy stage.

• Private endpoint on the storage account once the pattern is proven (the app pulls over the VNet; the pipeline needs its own route — service endpoint or an agent with access).

• Lifecycle policy on the container — build-numbered zips accumulate forever otherwise.

• Keep the inline-secret linter. It costs nothing and blocks the exact class of mistake that infests legacy workflow definitions.

• New workflows that replace still-running legacy ones should ship disabled and be enabled at cutover — never let two schedulers fire the same job.

Total elapsed time for us, including all six failures: one working session. Total time if you follow this article: about 30 minutes — 25 of which are waiting for an MSI installer and RBAC propagation.


Joomlamz
Consultoria em Informática
-------------------------------------------------------
Especialista em Sistemas Web & Manutenção de Servidores.
A desenvolver o novo AplPortal com suporte a PHP 8.
Precisa de ajuda profissional? Contacte-me.

Tags: