diff --git a/logicapp/README.md b/logicapp/README.md new file mode 100644 index 0000000..f08cca3 --- /dev/null +++ b/logicapp/README.md @@ -0,0 +1,169 @@ +# Logic App SAP to SQL Integration Template + +This Bicep template creates an Azure Logic App (Standard) that receives data from an SAP system and moves it to a SQL Database. + +## Architecture + +The template deploys the following Azure resources: + +- **Logic App Standard** - Hosts the workflow that processes SAP data and writes to SQL +- **App Service Plan (WorkflowStandard)** - Provides compute resources for the Logic App +- **Storage Account** - Required for Logic App Standard runtime state and configuration +- **SQL Server** - Database server to store the SAP data +- **SQL Database** - Database to hold the processed SAP data +- **SQL Firewall Rule** - Allows Azure services to access the SQL Server + +## Deployment + +### Prerequisites + +- Azure CLI or Azure PowerShell +- Bicep CLI +- An Azure subscription with appropriate permissions + +### Parameters + +| Parameter | Type | Description | Default | +|-----------|------|-------------|---------| +| `resourceLocation` | string | Azure region for deployment | `swedencentral` | +| `sapConnectionString` | string | Connection string for SAP system | `''` | +| `sqlServerAdminLogin` | string | SQL Server administrator login | `sqladmin` | +| `sqlServerAdminPassword` | securestring | SQL Server administrator password | Required | + +### Deploy the template + +```bash +# Deploy using Azure CLI +az deployment sub create \ + --location swedencentral \ + --template-file main.bicep \ + --parameters sqlServerAdminPassword='YourSecurePassword123!' + +# Deploy with custom parameters +az deployment sub create \ + --location swedencentral \ + --template-file main.bicep \ + --parameters resourceLocation='westeurope' \ + sqlServerAdminLogin='myadmin' \ + sqlServerAdminPassword='YourSecurePassword123!' \ + sapConnectionString='your-sap-connection-string' +``` + +## Workflow + +The Logic App includes a sample workflow (`workflow.json`) that: + +1. **Receives SAP Data** - HTTP trigger accepts JSON data from SAP system +2. **Parses Data** - Validates and extracts required fields +3. **Validates Fields** - Ensures required fields are present +4. **Inserts to SQL** - Calls stored procedure to insert data into SQL Database +5. **Returns Response** - Sends success/error response back to SAP system + +### Expected SAP Data Format + +```json +{ + "sapData": { + "documentType": "INVOICE", + "documentNumber": "INV-2024-001", + "customerCode": "CUST001", + "amount": 1500.00, + "currency": "USD", + "date": "2024-08-04T10:30:00Z", + "description": "Product sale invoice" + } +} +``` + +### SQL Database Setup + +Create the following table and stored procedure in your SQL Database: + +```sql +-- Create table to store SAP data +CREATE TABLE [dbo].[SAPData] ( + [Id] INT IDENTITY(1,1) PRIMARY KEY, + [DocumentType] NVARCHAR(50) NOT NULL, + [DocumentNumber] NVARCHAR(100) NOT NULL UNIQUE, + [CustomerCode] NVARCHAR(50) NOT NULL, + [Amount] DECIMAL(18,2) NOT NULL, + [Currency] NVARCHAR(3) NOT NULL, + [DocumentDate] DATETIME2 NOT NULL, + [Description] NVARCHAR(500), + [ProcessedDate] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), + [CreatedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE() +); + +-- Create stored procedure to insert data +CREATE PROCEDURE [dbo].[InsertSAPData] + @DocumentType NVARCHAR(50), + @DocumentNumber NVARCHAR(100), + @CustomerCode NVARCHAR(50), + @Amount DECIMAL(18,2), + @Currency NVARCHAR(3), + @DocumentDate DATETIME2, + @Description NVARCHAR(500) = NULL, + @ProcessedDate DATETIME2 +AS +BEGIN + SET NOCOUNT ON; + + INSERT INTO [dbo].[SAPData] ( + [DocumentType], + [DocumentNumber], + [CustomerCode], + [Amount], + [Currency], + [DocumentDate], + [Description], + [ProcessedDate] + ) + VALUES ( + @DocumentType, + @DocumentNumber, + @CustomerCode, + @Amount, + @Currency, + @DocumentDate, + @Description, + @ProcessedDate + ); +END +``` + +## Configuration + +After deployment: + +1. **Configure SAP Connection** - Update the Logic App's SAP connection settings +2. **Set up SQL Connection** - The SQL connection string is automatically configured +3. **Deploy Workflow** - Upload the `workflow.json` to your Logic App +4. **Test Integration** - Send test data from SAP to verify the flow + +## Security Considerations + +- The SQL Server is configured to allow Azure services access +- Connection strings are stored as Logic App application settings +- Consider using Azure Key Vault for sensitive configurations +- Enable Azure AD authentication for SQL Server in production + +## Monitoring + +The Logic App provides built-in monitoring through: +- Run history and status tracking +- Azure Monitor integration +- Application Insights (optional) +- Diagnostic logs + +## Customization + +You can customize this template by: +- Adding additional data validation +- Implementing error handling and retry logic +- Adding transformation logic for SAP data +- Integrating with additional systems +- Adding authentication mechanisms + +## Support + +This template provides a foundation for SAP to SQL integration. Customize based on your specific SAP system configuration and data requirements. \ No newline at end of file diff --git a/logicapp/logicapp.bicep b/logicapp/logicapp.bicep new file mode 100644 index 0000000..3e7d42c --- /dev/null +++ b/logicapp/logicapp.bicep @@ -0,0 +1,159 @@ +param resourceLocation string = 'swedencentral' +param sapConnectionString string = '' +param sqlServerAdminLogin string = 'sqladmin' +@secure() +param sqlServerAdminPassword string + +// Generate unique names for resources +var uniqueSuffix = uniqueString(resourceGroup().id) +var storageAccountName = 'st${uniqueSuffix}' +var logicAppName = 'logic-sap-sql-${uniqueSuffix}' +var sqlServerName = 'sql-${uniqueSuffix}' +var sqlDatabaseName = 'db-sap-data' +var servicePlanName = 'asp-${uniqueSuffix}' + +// Storage Account for Logic App Standard +module storageAccount 'br/public:avm/res/storage/storage-account:0.6.7' = { + name: '${uniqueString(deployment().name, resourceLocation)}-storage' + params: { + // Required parameters + name: storageAccountName + // Non-required parameters + location: resourceLocation + skuName: 'Standard_LRS' + kind: 'StorageV2' + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + supportsHttpsTrafficOnly: true + networkAcls: { + defaultAction: 'Allow' + } + } +} + +// App Service Plan for Logic App Standard +module servicePlan 'br/public:avm/res/web/serverfarm:0.4.1' = { + name: '${uniqueString(deployment().name, resourceLocation)}-serverfarm' + params: { + // Required parameters + name: servicePlanName + // Non-required parameters + location: resourceLocation + skuName: 'WS1' + skuTier: 'WorkflowStandard' + kind: 'elastic' + elasticScaleEnabled: true + maximumElasticWorkerCount: 20 + } +} + +// SQL Server +resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = { + name: sqlServerName + location: resourceLocation + properties: { + administratorLogin: sqlServerAdminLogin + administratorLoginPassword: sqlServerAdminPassword + version: '12.0' + minimalTlsVersion: '1.2' + publicNetworkAccess: 'Enabled' + } +} + +// SQL Server Firewall Rule to allow Azure services +resource sqlFirewallRule 'Microsoft.Sql/servers/firewallRules@2023-05-01-preview' = { + parent: sqlServer + name: 'AllowAllWindowsAzureIps' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } +} + +// SQL Database +resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = { + parent: sqlServer + name: sqlDatabaseName + location: resourceLocation + properties: { + collation: 'SQL_Latin1_General_CP1_CI_AS' + maxSizeBytes: 2147483648 // 2GB + } + sku: { + name: 'Basic' + tier: 'Basic' + } +} + +// Logic App Standard +module logicApp 'br/public:avm/res/web/site:0.11.0' = { + name: '${uniqueString(deployment().name, resourceLocation)}-logicapp' + params: { + // Required parameters + kind: 'functionapp,workflowapp' + name: logicAppName + serverFarmResourceId: servicePlan.outputs.resourceId + // Non-required parameters + location: resourceLocation + httpsOnly: true + siteConfig: { + netFrameworkVersion: 'v6.0' + use32BitWorkerProcess: false + ftpsState: 'Disabled' + minTlsVersion: '1.2' + appSettings: [ + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.outputs.name};AccountKey=${storageAccount.outputs.primaryAccessKey};EndpointSuffix=core.windows.net' + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.outputs.name};AccountKey=${storageAccount.outputs.primaryAccessKey};EndpointSuffix=core.windows.net' + } + { + name: 'WEBSITE_CONTENTSHARE' + value: toLower(logicAppName) + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' + } + { + name: 'AzureFunctionsJobHost__extensionBundle__id' + value: 'Microsoft.Azure.Functions.ExtensionBundle.Workflows' + } + { + name: 'AzureFunctionsJobHost__extensionBundle__version' + value: '[1.*, 2.0.0)' + } + { + name: 'APP_KIND' + value: 'workflowApp' + } + { + name: 'SAP_CONNECTION_STRING' + value: sapConnectionString + } + { + name: 'SQL_CONNECTION_STRING' + value: 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Initial Catalog=${sqlDatabaseName};Persist Security Info=False;User ID=${sqlServerAdminLogin};Password=${sqlServerAdminPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;' + } + ] + } + } +} + +// Outputs +output logicAppName string = logicApp.outputs.name +output logicAppId string = logicApp.outputs.resourceId +output sqlServerName string = sqlServer.name +output sqlDatabaseName string = sqlDatabase.name +output storageAccountName string = storageAccount.outputs.name \ No newline at end of file diff --git a/logicapp/logicapp.json b/logicapp/logicapp.json new file mode 100644 index 0000000..b654b91 --- /dev/null +++ b/logicapp/logicapp.json @@ -0,0 +1,203 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.177.2456", + "templateHash": "11791322217515469753" + } + }, + "parameters": { + "resourceLocation": { + "type": "string", + "defaultValue": "swedencentral" + }, + "sapConnectionString": { + "type": "string", + "defaultValue": "" + }, + "sqlServerAdminLogin": { + "type": "string", + "defaultValue": "sqladmin" + }, + "sqlServerAdminPassword": { + "type": "securestring" + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(resourceGroup().id)]", + "storageAccountName": "[format('st{0}', variables('uniqueSuffix'))]", + "logicAppName": "[format('logic-sap-sql-{0}', variables('uniqueSuffix'))]", + "sqlServerName": "[format('sql-{0}', variables('uniqueSuffix'))]", + "sqlDatabaseName": "db-sap-data", + "servicePlanName": "[format('asp-{0}', variables('uniqueSuffix'))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-01-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('resourceLocation')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false, + "supportsHttpsTrafficOnly": true, + "networkAcls": { + "defaultAction": "Allow" + } + } + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2023-01-01", + "name": "[variables('servicePlanName')]", + "location": "[parameters('resourceLocation')]", + "sku": { + "name": "WS1", + "tier": "WorkflowStandard" + }, + "kind": "elastic", + "properties": { + "elasticScaleEnabled": true, + "maximumElasticWorkerCount": 20 + } + }, + { + "type": "Microsoft.Sql/servers", + "apiVersion": "2023-05-01-preview", + "name": "[variables('sqlServerName')]", + "location": "[parameters('resourceLocation')]", + "properties": { + "administratorLogin": "[parameters('sqlServerAdminLogin')]", + "administratorLoginPassword": "[parameters('sqlServerAdminPassword')]", + "version": "12.0", + "minimalTlsVersion": "1.2", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.Sql/servers/firewallRules", + "apiVersion": "2023-05-01-preview", + "name": "[format('{0}/{1}', variables('sqlServerName'), 'AllowAllWindowsAzureIps')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "0.0.0.0" + }, + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers', variables('sqlServerName'))]" + ] + }, + { + "type": "Microsoft.Sql/servers/databases", + "apiVersion": "2023-05-01-preview", + "name": "[format('{0}/{1}', variables('sqlServerName'), variables('sqlDatabaseName'))]", + "location": "[parameters('resourceLocation')]", + "properties": { + "collation": "SQL_Latin1_General_CP1_CI_AS", + "maxSizeBytes": 2147483648 + }, + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers', variables('sqlServerName'))]" + ] + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2023-01-01", + "name": "[variables('logicAppName')]", + "location": "[parameters('resourceLocation')]", + "kind": "functionapp,workflowapp", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "siteConfig": { + "netFrameworkVersion": "v6.0", + "use32BitWorkerProcess": false, + "ftpsState": "Disabled", + "minTlsVersion": "1.2", + "appSettings": [ + { + "name": "AzureWebJobsStorage", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix=core.windows.net', variables('storageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix=core.windows.net', variables('storageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(variables('logicAppName'))]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~4" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "node" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "~18" + }, + { + "name": "AzureFunctionsJobHost__extensionBundle__id", + "value": "Microsoft.Azure.Functions.ExtensionBundle.Workflows" + }, + { + "name": "AzureFunctionsJobHost__extensionBundle__version", + "value": "[1.*, 2.0.0)" + }, + { + "name": "APP_KIND", + "value": "workflowApp" + }, + { + "name": "SAP_CONNECTION_STRING", + "value": "[parameters('sapConnectionString')]" + }, + { + "name": "SQL_CONNECTION_STRING", + "value": "[format('Server=tcp:{0},1433;Initial Catalog={1};Persist Security Info=False;User ID={2};Password={3};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;', reference(resourceId('Microsoft.Sql/servers', variables('sqlServerName')), '2023-05-01-preview').fullyQualifiedDomainName, variables('sqlDatabaseName'), parameters('sqlServerAdminLogin'), parameters('sqlServerAdminPassword'))]" + } + ] + }, + "httpsOnly": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "[resourceId('Microsoft.Sql/servers', variables('sqlServerName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ] + } + ], + "outputs": { + "logicAppName": { + "type": "string", + "value": "[variables('logicAppName')]" + }, + "logicAppId": { + "type": "string", + "value": "[resourceId('Microsoft.Web/sites', variables('logicAppName'))]" + }, + "sqlServerName": { + "type": "string", + "value": "[variables('sqlServerName')]" + }, + "sqlDatabaseName": { + "type": "string", + "value": "[variables('sqlDatabaseName')]" + }, + "storageAccountName": { + "type": "string", + "value": "[variables('storageAccountName')]" + } + } +} \ No newline at end of file diff --git a/logicapp/main.bicep b/logicapp/main.bicep new file mode 100644 index 0000000..79f8d7b --- /dev/null +++ b/logicapp/main.bicep @@ -0,0 +1,23 @@ +targetScope = 'subscription' + +param resourceLocation string = 'swedencentral' +param sapConnectionString string = '' +param sqlServerAdminLogin string = 'sqladmin' +@secure() +param sqlServerAdminPassword string + +resource rgLogicApp 'Microsoft.Resources/resourceGroups@2022-09-01' = { + name: 'rg-logicapp-sap-sql' + location: resourceLocation +} + +module logicAppResources './logicapp.bicep' = { + scope: rgLogicApp + name: 'logicapp-sap-sql' + params: { + resourceLocation: resourceLocation + sapConnectionString: sapConnectionString + sqlServerAdminLogin: sqlServerAdminLogin + sqlServerAdminPassword: sqlServerAdminPassword + } +} \ No newline at end of file diff --git a/logicapp/main.bicepparam b/logicapp/main.bicepparam new file mode 100644 index 0000000..d307ae7 --- /dev/null +++ b/logicapp/main.bicepparam @@ -0,0 +1,6 @@ +using 'main.bicep' + +param resourceLocation = 'swedencentral' +param sapConnectionString = '' +param sqlServerAdminLogin = 'sqladmin' +param sqlServerAdminPassword = 'YourSecurePassword123!' \ No newline at end of file diff --git a/logicapp/main.json b/logicapp/main.json new file mode 100644 index 0000000..f258f54 --- /dev/null +++ b/logicapp/main.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceLocation": { + "value": "swedencentral" + }, + "sapConnectionString": { + "value": "" + }, + "sqlServerAdminLogin": { + "value": "sqladmin" + }, + "sqlServerAdminPassword": { + "value": "YourSecurePassword123!" + } + } +} \ No newline at end of file diff --git a/logicapp/workflow.json b/logicapp/workflow.json new file mode 100644 index 0000000..1404523 --- /dev/null +++ b/logicapp/workflow.json @@ -0,0 +1,209 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "sapConnectionString": { + "type": "string", + "metadata": { + "description": "SAP connection string for receiving data" + } + }, + "sqlConnectionString": { + "type": "string", + "metadata": { + "description": "SQL Server connection string for writing data" + } + } + }, + "triggers": { + "SAP_Data_Received": { + "type": "HttpRequest", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "sapData": { + "type": "object", + "properties": { + "documentType": { + "type": "string" + }, + "documentNumber": { + "type": "string" + }, + "customerCode": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "date": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "documentType", + "documentNumber", + "customerCode", + "amount", + "currency", + "date" + ] + } + }, + "required": [ + "sapData" + ] + } + } + } + }, + "actions": { + "Parse_SAP_Data": { + "type": "ParseJson", + "inputs": { + "content": "@triggerBody()?['sapData']", + "schema": { + "type": "object", + "properties": { + "documentType": { + "type": "string" + }, + "documentNumber": { + "type": "string" + }, + "customerCode": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "date": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "runAfter": {} + }, + "Validate_Required_Fields": { + "type": "Condition", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@body('Parse_SAP_Data')?['documentNumber']", + null + ] + } + }, + { + "not": { + "equals": [ + "@body('Parse_SAP_Data')?['customerCode']", + null + ] + } + }, + { + "not": { + "equals": [ + "@body('Parse_SAP_Data')?['amount']", + null + ] + } + } + ] + }, + "actions": { + "Insert_Data_to_SQL": { + "type": "SqlServerStoredProcedure", + "inputs": { + "body": { + "DocumentType": "@body('Parse_SAP_Data')?['documentType']", + "DocumentNumber": "@body('Parse_SAP_Data')?['documentNumber']", + "CustomerCode": "@body('Parse_SAP_Data')?['customerCode']", + "Amount": "@body('Parse_SAP_Data')?['amount']", + "Currency": "@body('Parse_SAP_Data')?['currency']", + "DocumentDate": "@body('Parse_SAP_Data')?['date']", + "Description": "@coalesce(body('Parse_SAP_Data')?['description'], '')", + "ProcessedDate": "@utcNow()" + }, + "host": { + "connectionName": "sql", + "operationId": "executeStoredProcedure", + "apiId": "/subscriptions/@{workflow().subscriptionId}/providers/Microsoft.Web/locations/@{workflow().location}/managedApis/sql" + }, + "method": "post", + "path": "/v2/datasets/@{encodeURIComponent(encodeURIComponent('default'))}/procedures/@{encodeURIComponent(encodeURIComponent('[dbo].[InsertSAPData]'))}" + }, + "runAfter": {} + }, + "Success_Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "success", + "message": "SAP data successfully processed and inserted into SQL Database", + "documentNumber": "@body('Parse_SAP_Data')?['documentNumber']", + "processedAt": "@utcNow()" + } + }, + "runAfter": { + "Insert_Data_to_SQL": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": { + "Error_Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 400, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "error", + "message": "Missing required fields: documentNumber, customerCode, or amount", + "receivedData": "@body('Parse_SAP_Data')" + } + }, + "runAfter": {} + } + } + }, + "runAfter": { + "Parse_SAP_Data": [ + "Succeeded" + ] + } + } + }, + "outputs": {} + }, + "kind": "Stateful" +} \ No newline at end of file