Azure Logic Apps: workflows de integración sin código
Resumen
Logic Apps automatiza workflows de integración con 400+ conectores (Office 365, Salesforce, SAP, Twitter, SQL). Visual designer, triggers, actions, conditions, loops. En este post verás diseño de workflows, conectores críticos, error handling, y deployment con Bicep/ARM.
¿Qué es Logic Apps?
Integration Platform as a Service (iPaaS):
flowchart TD
Trigger[Trigger:
HTTP Request,
Timer, Email, Blob] --> Action1[Action 1:
Parse JSON]
Action1 --> Condition{Condition:
Status == 'approved'?}
Condition -->|Yes| Action2[Send Email
Outlook connector]
Condition -->|No| Action3[Post to Teams
Teams connector]
Action2 --> Action4[Insert row SQL
SQL connector]
Action3 --> Action4
Action4 --> End[End]
Casos de uso: - Procesar archivos al subirlos a Blob Storage - Integrar CRM (Salesforce) con ERP (SAP) - Aprobar facturas por email - Monitorear Twitter y alertar en Teams - ETL simple (extract, transform, load)
Standard vs Consumption
| Feature | Consumption | Standard |
|---|---|---|
| Hosting | Multi-tenant | Single-tenant (App Service Plan) |
| Pricing | Por ejecución | Por plan (fijo) |
| VNet integration | ❌ | ✅ |
| Stateful/Stateless | Solo Stateful | Ambos |
| Local development | Limitado | VS Code full support |
| Precio típico | $0.000025/acción | ~$200/mes |
Cuándo usar Standard: - High throughput (>1M actions/mes) - VNet integration requerida - Dev/test local con VS Code - Stateless workflows (mejor performance)
Crear Logic App (Consumption)
Portal Azure
# Crear Logic App Consumption
az logic workflow create \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--name logic-app-blob-processor \
--definition '{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"triggers": {},
"actions": {},
"outputs": {}
}
}'
Desde Portal: Azure Portal → Logic Apps → Create → Consumption → Designer
Workflow ejemplo: procesar blobs
Trigger: When a blob is added
{
"triggers": {
"When_a_blob_is_added": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azureblob']['connectionId']"
}
},
"method": "get",
"path": "/v2/datasets/@{encodeURIComponent('AccountNameFromSettings')}/triggers/batch/onupdatedfile",
"queries": {
"folderId": "/uploads",
"maxFileCount": 10,
"checkBothCreatedAndModifiedDateTime": false
}
},
"recurrence": {
"frequency": "Minute",
"interval": 5
}
}
}
}
Action: Get blob content
{
"actions": {
"Get_blob_content": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azureblob']['connectionId']"
}
},
"method": "get",
"path": "/v2/datasets/@{encodeURIComponent('AccountNameFromSettings')}/files/@{encodeURIComponent(triggerBody()?['Id'])}/content"
},
"runAfter": {}
}
}
}
Action: Parse JSON
{
"Parse_JSON": {
"type": "ParseJson",
"inputs": {
"content": "@body('Get_blob_content')",
"schema": {
"type": "object",
"properties": {
"orderId": {"type": "string"},
"customer": {"type": "string"},
"total": {"type": "number"}
}
}
},
"runAfter": {
"Get_blob_content": ["Succeeded"]
}
}
}
Action: Insert row (SQL)
{
"Insert_row_SQL": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['sql']['connectionId']"
}
},
"method": "post",
"path": "/v2/datasets/@{encodeURIComponent('default')}/tables/@{encodeURIComponent('[dbo].[Orders]')}/items",
"body": {
"OrderId": "@{body('Parse_JSON')?['orderId']}",
"Customer": "@{body('Parse_JSON')?['customer']}",
"Total": "@{body('Parse_JSON')?['total']}",
"ProcessedDate": "@{utcNow()}"
}
},
"runAfter": {
"Parse_JSON": ["Succeeded"]
}
}
}
Action: Send email (Outlook)
{
"Send_email_confirmation": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['outlook']['connectionId']"
}
},
"method": "post",
"path": "/v2/Mail",
"body": {
"To": "admin@example.com",
"Subject": "Order @{body('Parse_JSON')?['orderId']} processed",
"Body": "<p>Order details:<br>Customer: @{body('Parse_JSON')?['customer']}<br>Total: $@{body('Parse_JSON')?['total']}</p>",
"Importance": "Normal"
}
},
"runAfter": {
"Insert_row_SQL": ["Succeeded"]
}
}
}
Control flow: conditions, loops, scopes
Condition
{
"Condition_Check_Total": {
"type": "If",
"expression": {
"and": [
{
"greater": [
"@body('Parse_JSON')?['total']",
1000
]
}
]
},
"actions": {
"Send_approval_email": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['outlook']['connectionId']"
}
},
"method": "post",
"path": "/v2/Mail",
"body": {
"To": "manager@example.com",
"Subject": "Approval required for order @{body('Parse_JSON')?['orderId']}",
"Body": "Order total: $@{body('Parse_JSON')?['total']}"
}
}
}
},
"else": {
"actions": {
"Auto_approve": {
"type": "Response",
"inputs": {
"statusCode": 200,
"body": {"status": "auto-approved"}
}
}
}
}
}
}
ForEach loop
{
"ForEach_OrderItem": {
"type": "Foreach",
"foreach": "@body('Parse_JSON')?['items']",
"actions": {
"Insert_order_item": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['sql']['connectionId']"
}
},
"method": "post",
"path": "/v2/datasets/@{encodeURIComponent('default')}/tables/@{encodeURIComponent('[dbo].[OrderItems]')}/items",
"body": {
"OrderId": "@{body('Parse_JSON')?['orderId']}",
"ProductId": "@{items('ForEach_OrderItem')?['productId']}",
"Quantity": "@{items('ForEach_OrderItem')?['quantity']}"
}
}
}
}
}
}
Until loop
{
"Until_Job_Completed": {
"type": "Until",
"expression": "@equals(body('Check_Job_Status')?['status'], 'completed')",
"limit": {
"count": 60,
"timeout": "PT1H"
},
"actions": {
"Check_Job_Status": {
"type": "Http",
"inputs": {
"method": "GET",
"uri": "https://api.example.com/jobs/@{variables('jobId')}/status"
}
},
"Delay_30_seconds": {
"type": "Wait",
"inputs": {
"interval": {
"count": 30,
"unit": "Second"
}
},
"runAfter": {
"Check_Job_Status": ["Succeeded"]
}
}
}
}
}
Scope (agrupación y error handling)
{
"Scope_Database_Operations": {
"type": "Scope",
"actions": {
"Insert_Order": {
"type": "ApiConnection",
"inputs": { /* ... */ }
},
"Insert_OrderItems": {
"type": "ApiConnection",
"inputs": { /* ... */ },
"runAfter": {
"Insert_Order": ["Succeeded"]
}
}
}
},
"Scope_Error_Handler": {
"type": "Scope",
"actions": {
"Send_error_notification": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['teams']['connectionId']"
}
},
"method": "post",
"path": "/v3/messages",
"body": {
"channelId": "@parameters('teamsChannelId')",
"message": {
"subject": "Logic App Error",
"body": {
"content": "<p>Error in workflow: @{actions('Scope_Database_Operations')?['error']?['message']}</p>"
}
}
}
}
}
},
"runAfter": {
"Scope_Database_Operations": ["Failed", "Skipped", "TimedOut"]
}
}
}
Conectores críticos
HTTP connector (REST APIs)
{
"HTTP_Call_External_API": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://api.external.com/v1/orders",
"headers": {
"Authorization": "Bearer @{parameters('apiToken')}",
"Content-Type": "application/json"
},
"body": {
"orderId": "@{body('Parse_JSON')?['orderId']}",
"timestamp": "@{utcNow()}"
}
},
"retryPolicy": {
"type": "fixed",
"count": 4,
"interval": "PT20S"
}
}
}
Azure Functions connector
{
"Call_Azure_Function": {
"type": "Function",
"inputs": {
"function": {
"id": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/func-validator/functions/ValidateOrder"
},
"method": "POST",
"body": "@body('Parse_JSON')"
}
}
}
Service Bus connector
{
"Send_message_to_queue": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['servicebus']['connectionId']"
}
},
"method": "post",
"path": "/@{encodeURIComponent('orders-queue')}/messages",
"body": {
"ContentData": "@{base64(body('Parse_JSON'))}"
},
"queries": {
"systemProperties": "None"
}
}
}
}
Cosmos DB connector
{
"Create_or_update_document": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['cosmosdb']['connectionId']"
}
},
"method": "post",
"path": "/v2/cosmosdb/@{encodeURIComponent('OrdersDB')}/colls/@{encodeURIComponent('Orders')}/docs",
"body": {
"id": "@{body('Parse_JSON')?['orderId']}",
"customer": "@{body('Parse_JSON')?['customer']}",
"total": "@{body('Parse_JSON')?['total']}",
"timestamp": "@{utcNow()}"
}
}
}
}
Error handling y retry
Configure retry policy
{
"HTTP_with_retry": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://api.unreliable.com/data"
},
"retryPolicy": {
"type": "exponential",
"count": 4,
"interval": "PT10S",
"maximumInterval": "PT1H",
"minimumInterval": "PT5S"
}
}
}
Try-Catch pattern con Scopes
{
"Try_Block": {
"type": "Scope",
"actions": {
"Risky_Operation": {
"type": "Http",
"inputs": { /* ... */ }
}
}
},
"Catch_Block": {
"type": "Scope",
"actions": {
"Log_Error": {
"type": "AppendToStringVariable",
"inputs": {
"name": "errorLog",
"value": "@{actions('Risky_Operation')?['error']?['message']}"
}
},
"Send_Alert": {
"type": "ApiConnection",
"inputs": { /* Teams/Email */ }
}
},
"runAfter": {
"Try_Block": ["Failed", "Skipped", "TimedOut"]
}
}
}
Deployment con Bicep
Logic App Consumption
// logic-app.bicep
param location string = resourceGroup().location
param logicAppName string = 'logic-app-blob-processor'
param storageAccountConnectionString string
resource blobConnection 'Microsoft.Web/connections@2016-06-01' = {
name: 'azureblob'
location: location
properties: {
displayName: 'Azure Blob Storage'
api: {
id: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Web/locations/${location}/managedApis/azureblob'
}
parameterValues: {
accountName: 'stproddata'
accessKey: storageAccountConnectionString
}
}
}
resource logicApp 'Microsoft.Logic/workflows@2019-05-01' = {
name: logicAppName
location: location
properties: {
state: 'Enabled'
definition: {
'$schema': 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#'
contentVersion: '1.0.0.0'
parameters: {
'$connections': {
defaultValue: {}
type: 'Object'
}
}
triggers: {
When_a_blob_is_added: {
type: 'ApiConnection'
inputs: {
host: {
connection: {
name: '@parameters(\'$connections\')[\'azureblob\'][\'connectionId\']'
}
}
method: 'get'
path: '/v2/datasets/@{encodeURIComponent(\'AccountNameFromSettings\')}/triggers/batch/onupdatedfile'
}
}
}
actions: {
// Actions here...
}
}
parameters: {
'$connections': {
value: {
azureblob: {
connectionId: blobConnection.id
connectionName: 'azureblob'
id: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Web/locations/${location}/managedApis/azureblob'
}
}
}
}
}
}
Monitoring
Diagnostic logs
az monitor diagnostic-settings create \
--name logic-app-diagnostics \
--resource /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Logic/workflows/logic-app-blob-processor \
--logs '[{"category":"WorkflowRuntime","enabled":true}]' \
--workspace $LOG_ANALYTICS_ID
KQL queries
// Failed runs
AzureDiagnostics
| where ResourceType == "WORKFLOWS"
| where status_s == "Failed"
| project TimeGenerated, resource_workflowName_s, resource_runId_s, error_message_s
| order by TimeGenerated desc
// Run duration
AzureDiagnostics
| where ResourceType == "WORKFLOWS"
| where status_s == "Succeeded"
| extend Duration = endTime_t - startTime_t
| summarize AvgDuration = avg(Duration) by resource_workflowName_s
// Action success rate
AzureDiagnostics
| where ResourceType == "WORKFLOWS"
| summarize Total = count(), Failed = countif(status_s == "Failed") by resource_actionName_s
| extend SuccessRate = 100.0 * (Total - Failed) / Total
| order by SuccessRate asc
Buenas prácticas
Design: - ✅ Usar Scopes para agrupar actions relacionadas - ✅ Implement try-catch pattern con runAfter - ✅ Variables para datos compartidos entre actions - ✅ Parameters para configuraciones por entorno
Performance: - ✅ Stateless workflows para high throughput (Standard) - ✅ Batch triggers (procesar múltiples items juntos) - ✅ Until loops con timeout y count limit - ✅ Async pattern para operaciones largas
Security: - ✅ Managed Identity para conectores Azure - ✅ Key Vault para secrets (connection strings, API keys) - ✅ No hardcodear credentials en definition - ✅ Secure inputs/outputs (ocultar datos sensibles en logs)
Cost optimization: - ✅ Consumption para < 1M actions/mes - ✅ Standard con stateless para > 1M actions/mes - ✅ Evitar polling frecuente (usar Event Grid triggers)
Costes
Consumption:
Actions: $0.000025/action
Built-in actions: GRATIS
Enterprise connectors: $0.001/action
Ejemplo:
- 100,000 actions/mes: $2.50/mes
- 1M actions/mes: $25/mes
- Con enterprise connectors: $1,025/mes
Standard:
Workflow Standard S1:
- Plan: ~$200/mes (includes 100,000 actions)
- Additional actions: GRATIS
- Enterprise connectors: GRATIS
Break-even: ~8M actions/mes (vs Consumption)