diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 00000000..c309d438 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,109 @@ +# IPAM Deployment - Standardized Approach + +This directory contains the standardized deployment scripts for Azure IPAM (IP Address Management) that follow the established infrastructure deployment patterns. + +## Files Overview + +| File | Purpose | +|------|---------| +| `main.bicep` | Main Bicep template for IPAM infrastructure | +| `main.bicepparam` | Bicep parameter file with standardized naming | +| `MCase-L3-Apps-IPAM.ps1` | Standardized deployment script | +| `New-IPAMAppRegistrations.ps1` | App registration creation script | +| `deploy.ps1` | Original IPAM deployment script (legacy) | + +## Deployment Process + +### 1. Prerequisites + +- Azure PowerShell logged in with appropriate permissions +- Service Principal with required environment variables: + - `DEPLOYMENT_CLIENT_ID` + - `DEPLOYMENT_CLIENT_SECRET` + - `DEPLOYMENT_TENANT_ID` + +### 2. Create App Registrations (One-time setup) + +Before deploying the infrastructure, you need to create the Azure AD App Registrations: + +```powershell +# From repository root +.\submodules\ipam\deploy\New-IPAMAppRegistrations.ps1 -UIAppName "ipam-ui-app" -EngineAppName "ipam-engine-app" +``` + +This will: +- Create the IPAM UI and Engine App Registrations +- Configure required API permissions +- Generate a `main.parameters.json` file with the app IDs and secrets + +### 3. Update Bicep Parameters + +Copy the app registration details from the generated `main.parameters.json` into the `main.bicepparam` file: + +```bicep +// Required engine app registration parameters +param engineAppId = 'your-engine-app-id-here' +param engineAppSecret = 'your-engine-app-secret-here' +``` + +### 4. Deploy Infrastructure + +Deploy the IPAM infrastructure using the standardized script: + +```powershell +# From repository root - Deploy +.\submodules\ipam\deploy\MCase-L3-Apps-IPAM.ps1 + +# What-if deployment (dry run) +.\submodules\ipam\deploy\MCase-L3-Apps-IPAM.ps1 -WhatIfEnabled $true + +# Delete resources +.\submodules\ipam\deploy\MCase-L3-Apps-IPAM.ps1 -Delete +``` + +## Resource Naming Convention + +The deployment follows the established naming conventions: + +| Resource Type | Naming Pattern | Example | +|---------------|----------------|---------| +| Resource Group | `{client}-rg-{lc}-ipam` | `mcsdev001-rg-cu-ipam` | +| App Service | `{client}-app-ipam-01` | `mcsdev001-app-ipam-01` | +| Function App | `{client}-func-ipam-01` | `mcsdev001-func-ipam-01` | +| Key Vault | `{client}-kv-ipam-01` | `mcsdev001-kv-ipam-01` | +| Cosmos DB | `{client}-cosmos-ipam-01` | `mcsdev001-cosmos-ipam-01` | +| Log Analytics | `{client}-log-ipam-01` | `mcsdev001-log-ipam-01` | +| Managed Identity | `{client}-id-ipam-01` | `mcsdev001-id-ipam-01` | +| Storage Account | `{client}stipam01` | `mcsdev001stipam01` | +| Container Registry | `{client}cripam01` | `mcsdev001cripam01` | + +All naming follows the Azure resource abbreviation standards from `docs/references/azure-resource-types.md`. + +## Configuration + +The deployment is configured through: + +1. **`build.json`** - Central configuration (client, location, subscription) +2. **`main.bicepparam`** - IPAM-specific parameters and resource names +3. **Environment Variables** - Service Principal credentials + +## Key Features + +- ✅ **Standardized Structure**: Follows established deployment script patterns +- ✅ **Bicepparam Integration**: Uses `.bicepparam` files instead of JSON +- ✅ **Azure Standards**: Follows Microsoft naming conventions +- ✅ **Separation of Concerns**: App registration and infrastructure deployment are separate +- ✅ **What-If Support**: Dry-run capability for testing +- ✅ **Cleanup Support**: Delete operations for resource cleanup +- ✅ **Error Handling**: Comprehensive error handling and logging +- ✅ **Path Management**: Consistent relative path handling + +## Migration from Legacy Script + +The original `deploy.ps1` script handled multiple concerns in a single file. The new approach separates: + +1. **App Registration** → `New-IPAMAppRegistrations.ps1` +2. **Infrastructure Deployment** → `MCase-L3-Apps-IPAM.ps1` + `main.bicepparam` +3. **Container Building/ZIP Deployment** → Future separate scripts (not yet implemented) + +This separation improves maintainability and follows the established patterns used throughout the infrastructure codebase. \ No newline at end of file diff --git a/deploy/build.json b/deploy/build.json new file mode 100644 index 00000000..b9f80969 --- /dev/null +++ b/deploy/build.json @@ -0,0 +1,41 @@ +{ + "client": "connectivity", + "location": "centralus", + "azureCloud": "AzureCloud", + "lc": "cu", + "subscriptionName": "Azure-Comm-Connectivity", + "subscriptionId": "8ed4a7d5-04a8-48d0-a10d-20bf89e8b420", + "tenantId": "32599cbd-2936-4030-972f-1bdcfa8a673b", + "ipam": { + "config": { + "deployAsFunc": false, + "deployAsContainer": true, + "privateAcr": false, + "disableUI": false, + "tags": { + "Application": "IPAM", + "Environment": "Production", + "Client": "connectivity", + "Location": "centralus" + } + }, + "resourceNames": { + "resourceGroupName": "connectivity-rg-cu-ipam", + "functionName": "connectivity-func-ipam-01", + "appServiceName": "connectivity-app-ipam-01", + "functionPlanName": "connectivity-asp-ipam-func-01", + "appServicePlanName": "connectivity-asp-ipam-app-01", + "cosmosAccountName": "connectivity-cosmos-ipam-01", + "cosmosContainerName": "ipam-ctr", + "cosmosDatabaseName": "ipam-db", + "keyVaultName": "connectivity-kv-ipam-01", + "workspaceName": "connectivity-log-ipam-01", + "managedIdentityName": "connectivity-id-ipam-01", + "storageAccountName": "connectivitystipam01", + "containerRegistryName": "connectivitycripam01" + }, + "engineAppId": "8e2c41b7-1a74-4e1e-b2f7-60c85264c0ab", + "uiAppId": "3669b6bd-1c3e-4c14-a48c-6fb5d60feb2e", + "engineAppSecret": "REPLACE-WITH-ENGINE-SECRET" + } +} diff --git a/deploy/deploy.ps1 b/deploy/deploy.ps1 index 98202582..8df60006 100644 --- a/deploy/deploy.ps1 +++ b/deploy/deploy.ps1 @@ -1,6 +1,7 @@ ############################################################################################################### ## ## Azure IPAM Solution Deployment Script +## Simplified version that reads all configuration from build.json ## ############################################################################################################### @@ -10,1041 +11,344 @@ #Requires -Modules @{ ModuleName="Az.Functions"; ModuleVersion="4.0.6"} #Requires -Modules @{ ModuleName="Az.Resources"; ModuleVersion="6.10.0"} #Requires -Modules @{ ModuleName="Az.Websites"; ModuleVersion="3.1.1"} -#Requires -Modules @{ ModuleName="Microsoft.Graph.Authentication"; ModuleVersion="2.0.0"} -#Requires -Modules @{ ModuleName="Microsoft.Graph.Identity.SignIns"; ModuleVersion="2.0.0"} +#Requires -Modules @{ ModuleName="Az.KeyVault"; ModuleVersion="4.9.0"} -# Intake and set global parameters -[CmdletBinding(DefaultParameterSetName = 'AppContainer')] +# Simplified parameters - all configuration comes from build.json +[CmdletBinding()] param( - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $true, - ParameterSetName = 'App')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $true, - ParameterSetName = 'AppContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $true, - ParameterSetName = 'Function')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $true, - ParameterSetName = 'FunctionContainer')] - [string] - $Location, - - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'App')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'Function')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'FunctionContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppsOnly')] - [string] - $UIAppName = 'ipam-ui-app', - - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'App')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'Function')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'FunctionContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppsOnly')] - [string] - $EngineAppName = 'ipam-engine-app', - - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'App')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'Function')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'FunctionContainer')] - [ValidateLength(1, 7)] - [string] - $NamePrefix, - - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'App')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'Function')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'FunctionContainer')] - [hashtable] - $Tags, - - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $true, - ParameterSetName = 'AppsOnly')] - [switch] - $AppsOnly, - - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'App')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'Function')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'FunctionContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppsOnly')] - [switch] - $DisableUI, - - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'App')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'Function')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'FunctionContainer')] - [Parameter(ValueFromPipelineByPropertyName = $true, - Mandatory = $false, - ParameterSetName = 'AppsOnly')] - [ValidatePattern('^([\x21-\x7E]*)(?" $field -ForegroundColor Red - } - - foreach ($field in $missingFields) { - Write-Host "ERROR: Missing Field ->" $field -ForegroundColor Red - } - - Write-Host "ERROR: Please refer to the 'Naming Rules and Restrictions for Azure Resources'" -ForegroundColor Red - Write-Host "ERROR: " -ForegroundColor Red -NoNewline - Write-Host "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules" -ForegroundColor Yellow - Write-Host - - throw [System.ArgumentException]::New("One of the required resource names is missing or invalid.") - } +$debugSetting = $DEBUG_MODE ? 'Continue' : 'SilentlyContinue' - return -not ($invalidFields -or $missingFields) - }) +# Start Transcript +Start-Transcript -Path $transcriptLog - $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() - $attributeCollection.Add($attrApp) - $attributeCollection.Add($attrAppContainer) - $attributeCollection.Add($attrFunction) - $attributeCollection.Add($attrFunctionContainer) - $attributeCollection.Add($attrValidation) +Write-Host "INFO: Loading configuration from $BuildConfigPath" - $param = [System.Management.Automation.RuntimeDefinedParameter]::new('ResourceNames', [hashtable], $attributeCollection) - $paramDict = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() - $paramDict.Add('ResourceNames', $param) +# Load build configuration +if (-not (Test-Path $BuildConfigPath)) { + Write-Error "Build configuration file not found: $BuildConfigPath" + exit 1 +} - return $paramDict +$BUILD = Get-Content -Path $BuildConfigPath -Raw | ConvertFrom-Json -Depth 15 + +# Override configuration with command line parameters if provided +if ($Location) { $BUILD.location = $Location } +if ($DisableUI.IsPresent) { $BUILD.ipam.config.disableUI = $true } +if ($Native.IsPresent) { $BUILD.ipam.config.deployAsContainer = $false } + +# Extract configuration values +$deployLocation = $BUILD.location +$uiAppId = if (-not $BUILD.ipam.config.disableUI -and $BUILD.ipam.uiAppId) { $BUILD.ipam.uiAppId } else { [GUID]::Empty } +$engineAppId = $BUILD.ipam.engineAppId +$engineSecret = if ($EngineAppSecret) { + Write-Host "INFO: Using engine app secret from secure parameter" + $EngineAppSecret +} elseif ($BUILD.ipam.engineAppSecret) { + Write-Host "INFO: Using engine app secret from build.json (will be migrated to Key Vault)" + $BUILD.ipam.engineAppSecret +} else { + Write-Warning "No engine app secret provided" + $null } -begin { - $ResourceNames = $PSBoundParameters['ResourceNames'] +$resourceNames = $BUILD.ipam.resourceNames +$tags = $BUILD.ipam.config.tags +$deployAsFunc = $BUILD.ipam.config.deployAsFunc +$deployAsContainer = $BUILD.ipam.config.deployAsContainer +$privateAcr = $BUILD.ipam.config.privateAcr + +Write-Host "INFO: Configuration loaded successfully" +Write-Host "INFO: Location: $deployLocation" +Write-Host "INFO: Deployment Mode: $(if ($deployAsFunc) { 'Function App' } else { 'App Service' }) $(if (-not $deployAsContainer) { '(Native)' } else { '(Container)' })" +Write-Host "INFO: Resource Group: $($resourceNames.resourceGroupName)" +Write-Host "INFO: Engine App ID: $engineAppId" +if (-not $BUILD.ipam.config.disableUI) { + Write-Host "INFO: UI App ID: $uiAppId" } -process { - $AZURE_ENV_MAP = @{ - AzureCloud = "AZURE_PUBLIC" - AzureUSGovernment = "AZURE_US_GOV" - USSec = "AZURE_US_GOV_SECRET" - AzureGermanCloud = "AZURE_GERMANY" - AzureChinaCloud = "AZURE_CHINA" - } - - # Root Directory - $ROOT_DIR = (Get-Item $($MyInvocation.MyCommand.Path)).Directory.Parent.FullName - - # Minimum Required Azure CLI Version - $MIN_AZ_CLI_VER = [System.Version]'2.35.0' - - # Check for Debug Flag - $DEBUG_MODE = [bool]$PSCmdlet.MyInvocation.BoundParameters[“Debug”].IsPresent - - # Set preference variables - $ErrorActionPreference = "Stop" - $DebugPreference = 'SilentlyContinue' - $ProgressPreference = 'SilentlyContinue' - - # Hide Azure PowerShell SDK Warnings - $Env:SuppressAzurePowerShellBreakingChangeWarnings = $true - - # Hide Azure PowerShell SDK & Azure CLI Survey Prompts - $Env:AzSurveyMessage = $false - $Env:AZURE_CORE_SURVEY_MESSAGE = $false - - # Set Log File Location - $logPath = Join-Path -Path $ROOT_DIR -ChildPath "logs" - New-Item -ItemType Directory -Path $logpath -Force | Out-Null - - $debugLog = Join-Path -Path $logPath -ChildPath "debug_$(get-date -format `"yyyyMMddhhmmsstt`").log" - $errorLog = Join-Path -Path $logPath -ChildPath "error_$(get-date -format `"yyyyMMddhhmmsstt`").log" - $transcriptLog = Join-Path -Path $logPath -ChildPath "deploy_$(get-date -format `"yyyyMMddhhmmsstt`").log" - - $debugSetting = $DEBUG_MODE ? 'Continue' : 'SilentlyContinue' - - $containerBuildError = $false - $deploymentSuccess = $false - - $GitHubUserName = 'Azure' - $GitHubRepoName = 'ipam' - $ZipFileName = 'ipam.zip' - $TempFolderObj = $null - - Start-Transcript -Path $transcriptLog | Out-Null - - Function Test-Location { - Param( - [Parameter(Mandatory = $true)] - [string]$Location - ) - - $validLocations = Get-AzLocation | Select-Object -ExpandProperty Location - - return $validLocations.Contains($Location) - } - Function Get-BuildLogs { - Param( - [Parameter(Mandatory = $true)] - [string]$SubscriptionId, - [Parameter(Mandatory = $true)] - [string]$ResourceGroupName, - [Parameter(Mandatory = $true)] - [string]$RegistryName, - [Parameter(Mandatory = $true)] - [string]$BuildId, - [Parameter(Mandatory = $true)] - [string]$AzureCloud - ) - - $msArmMap = @{ - AZURE_PUBLIC = "management.azure.com" - AZURE_US_GOV = "management.usgovcloudapi.net" - AZURE_US_GOV_SECRET = "management.azure.microsoft.scloud" - AZURE_GERMANY = "management.microsoftazure.de" - AZURE_CHINA = "management.chinacloudapi.cn" - } - - $accessToken = (Get-AzAccessToken).Token | ConvertTo-SecureString -AsPlainText - - $response = Invoke-RestMethod ` - -Method POST ` - -Uri "https://$($msArmMap[$AzureCloud])/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.ContainerRegistry/registries/$RegistryName/runs/$BuildId/listLogSasUrl?api-version=2019-04-01" ` - -Authentication Bearer ` - -Token $accessToken - - $logLink = $response.logLink - - $logs = Invoke-RestMethod ` - -Method GET ` - -Uri $logLink - - return $logs - } - - Function Deploy-IPAMApplications { - Param( - [Parameter(Mandatory = $false)] - [string]$EngineAppName = 'ipam-engine-app', - [Parameter(Mandatory = $false)] - [string]$UIAppName = 'ipam-ui-app', - [Parameter(Mandatory = $true)] - [string]$MgmtGroupId, - [Parameter(Mandatory = $true)] - [string]$AzureCloud, - [Parameter(Mandatory = $false)] - [bool]$DisableUI = $false - ) - - $uiResourceAccess = [System.Collections.ArrayList]@( - @{ - ResourceAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph - ResourceAccess = @( - @{ - Id = "37f7f235-527c-4136-accd-4a02d197296e" # openid - Type = "Scope" - }, - @{ - Id = "14dad69e-099b-42c9-810b-d002981feec1" # profile - Type = "Scope" - }, - @{ - Id = "7427e0e9-2fba-42fe-b0c0-848c9e6a8182" # offline_access - Type = "Scope" - }, - @{ - Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" # User.Read - Type = "Scope" - }, - @{ - Id = "06da0dbc-49e2-44d2-8312-53f166ab848a" # Directory.Read.All - Type = "Scope" - } - ) - } - ) - - # Create IPAM UI Application (If DisableUI not specified) - if (-not $DisableUI) { - Write-Host "INFO: Creating Azure IPAM UI Application" -ForegroundColor Green - - $uiApp = New-AzADApplication ` - -DisplayName $UiAppName ` - -SPARedirectUri "https://replace-this-value.azurewebsites.net" - } +# Validate required configuration +if (-not $engineAppId) { + Write-Error "Engine App ID is required in build.json (ipam.engineAppId)" + exit 1 +} - $engineResourceMap = @{ - "AZURE_PUBLIC" = @{ - ResourceAppId = "797f4846-ba00-4fd7-ba43-dac1f8f63013" # Azure Service Management - ResourceAccessIds = @("41094075-9dad-400e-a0bd-54e686782033") # user_impersonation - } - "AZURE_US_GOV" = @{ - ResourceAppId = "40a69793-8fe6-4db1-9591-dbc5c57b17d8" # Azure Service Management - ResourceAccessIds = @("8eb49ffc-05ac-454c-9027-8648349217dd", "e59ee429-1fb1-4054-b99f-f542e8dc9b95") # user_impersonation - } - "AZURE_US_GOV_SECRET" = @{ - ResourceAppId = "797f4846-ba00-4fd7-ba43-dac1f8f63013" # Azure Service Management - ResourceAccessIds = @("41094075-9dad-400e-a0bd-54e686782033") # user_impersonation - } - "AZURE_GERMANY" = @{ - ResourceAppId = "797f4846-ba00-4fd7-ba43-dac1f8f63013" # Azure Service Management - ResourceAccessIds = @("41094075-9dad-400e-a0bd-54e686782033") # user_impersonation - } - "AZURE_CHINA" = @{ - ResourceAppId = "797f4846-ba00-4fd7-ba43-dac1f8f63013" # Azure Service Management - ResourceAccessIds = @("41094075-9dad-400e-a0bd-54e686782033") # user_impersonation - } - } +if (-not $engineSecret) { + Write-Error "Engine App Secret is required in build.json (ipam.engineAppSecret)" + exit 1 +} - $engineResourceAppId = $engineResourceMap[$AzureCloud].ResourceAppId - $engineResourceAccess = [System.Collections.ArrayList]@() +$AZURE_ENV_MAP = @{ + AzureCloud = "AZURE_PUBLIC" + AzureUSGovernment = "AZURE_US_GOV" + USSec = "AZURE_US_GOV_SECRET" + AzureGermanCloud = "AZURE_GERMANY" + AzureChinaCloud = "AZURE_CHINA" +} - foreach ($engineAccessId in $engineResourceMap[$AzureCloud].ResourceAccessIds) { - $access = @{ - Id = $engineAccessId - Type = "Scope" - } +$azureCloud = $AZURE_ENV_MAP[$BUILD.azureCloud] - $engineResourceAccess.Add($access) | Out-Null - } +if (-not $azureCloud) { + Write-Error "Azure Cloud type is not currently supported: $($BUILD.azureCloud)" + exit 1 +} - $engineResourceAccessList = [System.Collections.ArrayList]@( - @{ - ResourceAppId = $engineResourceAppId - ResourceAccess = $engineResourceAccess - } - ) +# Load Key Vault secret management function +$keyVaultScriptPath = Join-Path $ROOT_DIR "../../scripts/pwsh/Infrastructure/KeyVault/Set-KeyvaultSecret.ps1" +if (Test-Path $keyVaultScriptPath) { + . $keyVaultScriptPath +} else { + Write-Warning "Key Vault script not found at: $keyVaultScriptPath" +} - $engineApiGuid = New-Guid +# Deploy-Bicep Function (simplified to use build.json parameters directly) +Function Deploy-Bicep { + Write-Host "INFO: Deploying IPAM bicep templates with JSON configuration" - $knownClientApplication = @( - $uiApp.AppId - ) + $DebugPreference = $debugSetting - $engineApiSettings = @{ - Oauth2PermissionScope = @( - @{ - AdminConsentDescription = "Allows the IPAM UI to access IPAM Engine API as the signed-in user." - AdminConsentDisplayName = "Access IPAM Engine API" - Id = $engineApiGuid - IsEnabled = $true - Type = "User" - UserConsentDescription = "Allow the IPAM UI to access IPAM Engine API on your behalf." - UserConsentDisplayName = "Access IPAM Engine API" - Value = "access_as_user" + # Build parameter object for bicep template + $templateParams = @{ + engineAppId = $engineAppId + engineAppSecret = $engineSecret + BUILD = $BUILD + ipamSpoke = @{ + resourceGroup = @{ + name = $resourceNames.resourceGroupName + } + functionApp = @{ + name = $resourceNames.functionName + } + appService = @{ + name = $resourceNames.appServiceName + } + appServicePlan = @{ + function = $resourceNames.functionPlanName + app = $resourceNames.appServicePlanName + } + cosmosDb = @{ + accountName = $resourceNames.cosmosAccountName + containerName = $resourceNames.cosmosContainerName + databaseName = $resourceNames.cosmosDatabaseName + } + keyVault = @{ + name = $resourceNames.keyVaultName + } + logAnalytics = @{ + name = $resourceNames.workspaceName + } + managedIdentity = @{ + name = $resourceNames.managedIdentityName + } + storageAccount = @{ + name = $resourceNames.storageAccountName + } + containerRegistry = @{ + name = $resourceNames.containerRegistryName + } } - ) - # Allow Azure PowerShell/CLI to obtain access tokens - PreAuthorizedApplication = @( - @{ - AppId = "1950a258-227b-4e31-a9cf-717495945fc2" # Azure PowerShell - DelegatedPermissionId = @( $engineApiGuid ) - }, - @{ - AppId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Azure CLI - DelegatedPermissionId = @( $engineApiGuid ) + ipamConfig = @{ + location = $deployLocation + azureCloud = $BUILD.azureCloud + privateAcr = $privateAcr + deployAsFunc = $deployAsFunc + deployAsContainer = $deployAsContainer + uiAppId = $uiAppId + tags = $tags } - ) - RequestedAccessTokenVersion = 2 - } - - # Add the UI App as a Known Client App (If DisableUI not specified) - if (-not $DisableUI) { - $engineApiSettings.Add("KnownClientApplication", $knownClientApplication) - } - - Write-Host "INFO: Creating Azure IPAM Engine Application" -ForegroundColor Green - - # Create IPAM Engine Application - $engineApp = New-AzADApplication ` - -DisplayName $EngineAppName ` - -Api $engineApiSettings ` - -RequiredResourceAccess $engineResourceAccessList - - Write-Host "INFO: Updating Azure IPAM Engine API Endpoint" -ForegroundColor Green - - # Update IPAM Engine API Endpoint - Update-AzADApplication -ApplicationId $engineApp.AppId -IdentifierUri "api://$($engineApp.AppId)" - - $uiEngineApiAccess = @{ - ResourceAppId = $engineApp.AppId - ResourceAccess = @( - @{ - Id = $engineApiGuid - Type = "Scope" - } - ) - } - - $uiResourceAccess.Add($uiEngineApiAccess) | Out-Null - - # Update IPAM UI Application Resource Access (If DisableUI not specified) - if (-not $DisableUI) { - Write-Host "INFO: Updating Azure IPAM UI Application Resource Access" -ForegroundColor Green - - Update-AzADApplication -ApplicationId $uiApp.AppId -RequiredResourceAccess $uiResourceAccess - - $uiObject = Get-AzADApplication -ApplicationId $uiApp.AppId - } - - $engineObject = Get-AzADApplication -ApplicationId $engineApp.AppId - - # Create IPAM UI Service Principal (If DisableUI not specified) - if (-not $DisableUI) { - Write-Host "INFO: Creating Azure IPAM UI Service Principal" -ForegroundColor Green - - New-AzADServicePrincipal -ApplicationObject $uiObject | Out-Null - } - - $scope = "/providers/Microsoft.Management/managementGroups/$MgmtGroupId" - - Write-Host "INFO: Creating Azure IPAM Engine Service Principal" -ForegroundColor Green - - # Create IPAM Engine Service Principal - New-AzADServicePrincipal -ApplicationObject $engineObject ` - -Role "Reader" ` - -Scope $scope ` - | Out-Null - - Write-Host "INFO: Creating Azure IPAM Engine Secret" -ForegroundColor Green - - # Create IPAM Engine Secret - $engineSecret = New-AzADAppCredential -ApplicationObject $engineObject -StartDate (Get-Date) -EndDate (Get-Date).AddYears(2) - - if (-not $DisableUI) { - Write-Host "INFO: Azure IPAM Engine & UI Applications/Service Principals created successfully" -ForegroundColor Green - } - else { - Write-Host "INFO: Azure IPAM Engine Application/Service Principal created successfully" -ForegroundColor Green - } - - $appDetails = @{ - EngineAppId = $engineApp.AppId - EngineSecret = $engineSecret.SecretText - } - - # Add UI AppID to AppDetails (If DisableUI not specified) - if (-not $DisableUI) { - $appDetails.Add("UIAppId", $uiApp.AppId) - } - - return $appDetails - } - - Function Grant-AdminConsent { - Param( - [Parameter(Mandatory = $false)] - [string]$UIAppId = [GUID]::Empty, - [Parameter(Mandatory = $true)] - [string]$EngineAppId, - [Parameter(Mandatory = $true)] - [string]$AzureCloud, - [Parameter(Mandatory = $false)] - [bool]$DisableUI = $false - ) - - $msGraphMap = @{ - AZURE_PUBLIC = @{ - Endpoint = "graph.microsoft.com" - Environment = "Global" - } - AZURE_US_GOV = @{ - Endpoint = "graph.microsoft.us" - Environment = "USGov" - } - AZURE_US_GOV_SECRET = @{ - Endpoint = "graph.cloudapi.microsoft.scloud" - Environment = "USSec" - } - AZURE_GERMANY = @{ - Endpoint = "graph.microsoft.de" - Environment = "Germany" - } - AZURE_CHINA = @{ - Endpoint = "microsoftgraph.chinacloudapi.cn" - Environment = "China" - } - } - - $uiGraphScopes = [System.Collections.ArrayList]@( - @{ - scopeId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph - scopes = "openid profile offline_access User.Read Directory.Read.All" - } - ) - - $engineGraphScopes = [System.Collections.ArrayList]@( - @{ - scopeId = "797f4846-ba00-4fd7-ba43-dac1f8f63013" # Azure Service Management - scopes = "user_impersonation" - } - ) - - # Get Microsoft Graph Access Token - $accesstoken = (Get-AzAccessToken -Resource "https://$($msGraphMap[$AzureCloud].Endpoint)/").Token - - # Switch Access Token to SecureString if Graph Version is 2.x - $graphVersion = [System.Version](Get-InstalledModule -Name Microsoft.Graph -ErrorAction SilentlyContinue | Sort-Object -Property Version | Select-Object -Last 1).Version ` - ?? (Get-Module -Name Microsoft.Graph -ErrorAction SilentlyContinue | Sort-Object -Property Version | Select-Object -Last 1).Version ` - ?? (Get-Module -Name Microsoft.Graph -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property Version | Select-Object -Last 1).Version ` - ?? [System.Version]([array](Get-InstalledModule | Where-Object { $_.Name -like "Microsoft.Graph.*" } | Select-Object -ExpandProperty Version | Sort-Object | Get-Unique))[0] - - if ($graphVersion.Major -gt 1) { - $accesstoken = ConvertTo-SecureString $accesstoken -AsPlainText -Force - } - - Write-Host "INFO: Logging in to Microsoft Graph" -ForegroundColor Green - - # Connect to Microsoft Graph - Connect-MgGraph -Environment $msGraphMap[$AzureCloud].Environment -AccessToken $accesstoken | Out-Null - - # Fetch Azure IPAM UI Service Principal (If DisableUI not specified) - if (-not $DisableUI) { - $uiSpn = Get-AzADServicePrincipal ` - -ApplicationId $UIAppId - } - - # Fetch Azure IPAM Engine Service Principal - $engineSpn = Get-AzADServicePrincipal ` - -ApplicationId $EngineAppId - - # Grant admin consent for Microsoft Graph API permissions assigned to IPAM UI application (If DisableUI not specified) - if (-not $DisableUI) { - Write-Host "INFO: Granting admin consent for Microsoft Graph API permissions assigned to IPAM UI application" -ForegroundColor Green - - foreach ($scope in $uiGraphScopes) { - $msGraphId = Get-AzADServicePrincipal ` - -ApplicationId $scope.scopeId - - New-MgOauth2PermissionGrant ` - -ResourceId $msGraphId.Id ` - -Scope $scope.scopes ` - -ClientId $uiSpn.Id ` - -ConsentType AllPrincipals ` - | Out-Null - } - - Write-Host "INFO: Admin consent for Microsoft Graph API permissions granted successfully" -ForegroundColor Green - } - - # Grant admin consent to the IPAM UI application for exposed API from the IPAM Engine application (If DisableUI not specified) - if (-not $DisableUI) { - Write-Host "INFO: Granting admin consent to the IPAM UI application for exposed API from the IPAM Engine application" -ForegroundColor Green - - New-MgOauth2PermissionGrant ` - -ResourceId $engineSpn.Id ` - -Scope "access_as_user" ` - -ClientId $uiSpn.Id ` - -ConsentType AllPrincipals ` - | Out-Null - - Write-Host "INFO: Admin consent for IPAM Engine exposed API granted successfully" -ForegroundColor Green - } - - Write-Host "INFO: Granting admin consent for Azure Service Management API permissions assigned to IPAM Engine application" -ForegroundColor Green - - # Grant admin consent for Azure Service Management API permissions assigned to IPAM Engine application - foreach ($scope in $engineGraphScopes) { - $msGraphId = Get-AzADServicePrincipal ` - -ApplicationId $scope.scopeId - - New-MgOauth2PermissionGrant ` - -ResourceId $msGraphId.Id ` - -Scope $scope.scopes ` - -ClientId $engineSpn.Id ` - -ConsentType AllPrincipals ` - | Out-Null - } - - Write-Host "INFO: Admin consent for Azure Service Management API permissions granted successfully" -ForegroundColor Green - } - - Function Save-Parameters { - Param( - [Parameter(Mandatory = $false)] - [string]$UIAppId = [GUID]::Empty, - [Parameter(Mandatory = $true)] - [string]$EngineAppId, - [Parameter(Mandatory = $true)] - [string]$EngineSecret, - [Parameter(Mandatory = $false)] - [bool]$DisableUI = $false - ) - - Write-Host "INFO: Populating Bicep parameter file for infrastructure deployment" -ForegroundColor Green - - # Retrieve JSON object from sample parameter file - $parametersObject = Get-Content main.parameters.example.json | ConvertFrom-Json - - # Update Parameter Values - $parametersObject.parameters.engineAppId.value = $EngineAppId - $parametersObject.parameters.engineAppSecret.value = $EngineSecret - - if (-not $DisableUI) { - $parametersObject.parameters.uiAppId.value = $UIAppId - $parametersObject.parameters = $parametersObject.parameters | Select-Object -Property uiAppId, engineAppId, engineAppSecret } - else { - $parametersObject.parameters = $parametersObject.parameters | Select-Object -Property engineAppId, engineAppSecret - } - - # Output updated parameter file for Bicep deployment - $parametersObject | ConvertTo-Json -Depth 4 | Out-File -FilePath main.parameters.json - - Write-Host "INFO: Bicep parameter file populated successfully" -ForegroundColor Green - } - Function Import-Parameters { - Param( - [Parameter(Mandatory = $true)] - [System.IO.FileInfo]$ParameterFile - ) + Write-Host "INFO: Deploying bicep template with verbose output enabled..." + Write-Host "INFO: Deployment Name: ipamInfraDeploy-$(Get-Date -Format `"yyyyMMddhhmmsstt`")" + Write-Host "INFO: Location: $deployLocation" + Write-Host "INFO: Template File: main.bicep" - Write-Host "INFO: Importing values from Bicep parameters file" -ForegroundColor Green - - # Retrieve JSON object from sample parameter file - $parametersObject = Get-Content $ParameterFile | ConvertFrom-Json - - # Read Values from Parameters - $UIAppId = $parametersObject.parameters.uiAppId.value ?? [GUID]::Empty - $EngineAppId = $parametersObject.parameters.engineAppId.value - $EngineSecret = $parametersObject.parameters.engineAppSecret.value - $script:DisableUI = ($UIAppId -eq [GUID]::Empty) ? $true : $false - - if ((-not $EngineAppId) -or (-not $EngineSecret)) { - Write-Host "ERROR: Missing required parameters from Bicep parameter file" -ForegroundColor Red - Write-Host "ERROR: Please ensure the following parameters are present in the Bicep parameter file" -ForegroundColor Red - Write-Host "ERROR: Required: [engineAppId, engineAppSecret]" -ForegroundColor Red - Write-Host "" - Write-Host "ERROR: Please refer to the deployment documentation for more information" -ForegroundColor Red - Write-Host "ERROR: " -ForegroundColor Red -NoNewline - Write-Host "https://azure.github.io/ipam/#/deployment/README" -ForegroundColor Yellow - Write-Host "" - - throw [System.ArgumentException]::New("One of the required parameters are missing or invalid.") - } - - # $deployType = $script:AsFunction ? 'Function' : 'Full' + # Deploy IPAM bicep template with parameters and verbose output + $deployment = New-AzSubscriptionDeployment ` + -Name "ipamInfraDeploy-$(Get-Date -Format `"yyyyMMddhhmmsstt`")" ` + -Location $deployLocation ` + -TemplateFile "main.bicep" ` + -TemplateParameterObject $templateParams ` + -Verbose - Write-Host "INFO: Successfully import Bicep parameter values for deployment" -ForegroundColor Green + $DebugPreference = 'SilentlyContinue' - $appDetails = @{ - UIAppId = $UIAppId - EngineAppId = $EngineAppId - EngineSecret = $EngineSecret - } + Write-Host "INFO: IPAM bicep templates deployed successfully" + Write-Host "INFO: Deployment ID: $($deployment.Id)" + Write-Host "INFO: Deployment State: $($deployment.ProvisioningState)" - return $appDetails - } + return $deployment +} - Function Deploy-Bicep { +Function Get-ZipFile { Param( - [Parameter(Mandatory = $false)] - [string]$UIAppId = [GUID]::Empty, - [Parameter(Mandatory = $true)] - [string]$EngineAppId, - [Parameter(Mandatory = $true)] - [string]$EngineSecret, - [Parameter(Mandatory = $false)] - [string]$NamePrefix, - [Parameter(Mandatory = $false)] - [string]$AzureCloud, - [Parameter(Mandatory = $false)] - [bool]$Function, - [Parameter(Mandatory = $false)] - [bool]$Native, - [Parameter(Mandatory = $false)] - [bool]$PrivateAcr, - [Parameter(Mandatory = $false)] - [hashtable]$Tags, - [Parameter(Mandatory = $false)] - [hashtable]$ResourceNames + [Parameter(Mandatory = $true)] + [string]$GitHubUserName, + [Parameter(Mandatory = $true)] + [string]$GitHubRepoName, + [Parameter(Mandatory = $true)] + [string]$ZipFileName, + [Parameter(Mandatory = $true)] + [System.IO.DirectoryInfo]$AssetFolder ) - Write-Host "INFO: Deploying IPAM bicep templates" -ForegroundColor Green - - # Instantiate deployment parameter object - $deploymentParameters = @{ - engineAppId = $EngineAppId - engineAppSecret = $EngineSecret - uiAppId = $UiAppId - } - - if ($NamePrefix) { - $deploymentParameters.Add('namePrefix', $NamePrefix) - } - - if ($AzureCloud) { - $deploymentParameters.Add('azureCloud', $AzureCloud) - } - - if ($Function) { - $deploymentParameters.Add('deployAsFunc', $Function) - } - - if (-not $Native) { - $deploymentParameters.Add('deployAsContainer', !$Native) - } - - if ($PrivateAcr) { - $deploymentParameters.Add('privateAcr', $PrivateAcr) - } - - if ($Tags) { - $deploymentParameters.Add('tags', $Tags) - } - - if ($ResourceNames) { - $deploymentParameters.Add('resourceNames', $ResourceNames) - } - - $DebugPreference = $debugSetting + $ZipFilePath = Join-Path -Path $AssetFolder.FullName -ChildPath $ZipFileName - # Deploy IPAM bicep template - $deployment = & { - New-AzSubscriptionDeployment ` - -Name "ipamInfraDeploy-$(Get-Date -Format `"yyyyMMddhhmmsstt`")" ` - -Location $location ` - -TemplateFile main.bicep ` - -TemplateParameterObject $deploymentParameters ` - 5>$($DEBUG_MODE ? $debugLog : $null) - } + try { + $GitHubURL = "https://api.github.com/repos/$GitHubUserName/$GitHubRepoName/releases/latest" - $DebugPreference = 'SilentlyContinue' + Write-Host "INFO: Target GitHub Repo is " -ForegroundColor Green -NoNewline + Write-Host "$GitHubUserName/$GitHubRepoName" -ForegroundColor Cyan + Write-Host "INFO: Fetching download URL..." -ForegroundColor Green - Write-Host "INFO: IPAM bicep templates deployed successfully" -ForegroundColor Green + $GHResponse = Invoke-WebRequest -Method GET -Uri $GitHubURL + $JSONResponse = $GHResponse.Content | ConvertFrom-Json + $AssetList = $JSONResponse.assets + $Asset = $AssetList | Where-Object { $_.name -eq $ZipFileName } + $DownloadURL = $Asset.browser_download_url - return $deployment - } + Write-Host "INFO: Downloading ZIP Archive to " -ForegroundColor Green -NoNewline + Write-Host $ZipFilePath -ForegroundColor Cyan - Function Get-ZipFile { - Param( - [Parameter(Mandatory = $true)] - [string]$GitHubUserName, - [Parameter(Mandatory = $true)] - [string]$GitHubRepoName, - [Parameter(Mandatory = $true)] - [string]$ZipFileName, - [Parameter(Mandatory = $true)] - [System.IO.DirectoryInfo]$AssetFolder - ) - - $ZipFilePath = Join-Path -Path $AssetFolder.FullName -ChildPath $ZipFileName - - try { - $GitHubURL = "https://api.github.com/repos/$GitHubUserName/$GitHubRepoName/releases/latest" - - Write-Host "INFO: Target GitHub Repo is " -ForegroundColor Green -NoNewline - Write-Host "$GitHubUserName/$GitHubRepoName" -ForegroundColor Cyan - Write-Host "INFO: Fetching download URL..." -ForegroundColor Green - - $GHResponse = Invoke-WebRequest -Method GET -Uri $GitHubURL - $JSONResponse = $GHResponse.Content | ConvertFrom-Json - $AssetList = $JSONResponse.assets - $Asset = $AssetList | Where-Object { $_.name -eq $ZipFileName } - $DownloadURL = $Asset.browser_download_url - - Write-Host "INFO: Downloading ZIP Archive to " -ForegroundColor Green -NoNewline - Write-Host $ZipFilePath -ForegroundColor Cyan - - Invoke-WebRequest -Uri $DownloadURL -OutFile $ZipFilePath + Invoke-WebRequest -Uri $DownloadURL -OutFile $ZipFilePath } catch { - Write-Host "ERROR: Unable to download ZIP Deploy archive!" -ForegroundColor Red - throw $_ + Write-Host "ERROR: Unable to download ZIP Deploy archive!" -ForegroundColor Red + throw $_ } - } +} - Function Publish-ZipFile { +Function Publish-ZipFile { Param( - [Parameter(Mandatory = $true)] - [string]$AppName, - [Parameter(Mandatory = $true)] - [string]$ResourceGroupName, - [Parameter(Mandatory = $true)] - [System.IO.FileInfo]$ZipFilePath, - [Parameter(Mandatory = $false)] - [switch]$UseAPI + [Parameter(Mandatory = $true)] + [string]$AppName, + [Parameter(Mandatory = $true)] + [string]$ResourceGroupName, + [Parameter(Mandatory = $true)] + [System.IO.FileInfo]$ZipFilePath, + [Parameter(Mandatory = $false)] + [switch]$UseAPI ) - if ($UseAPI) { - Write-Host "INFO: Using Kudu API for ZIP Deploy" -ForegroundColor Green - } - $publishRetries = 3 $publishSuccess = $False - if ($UseAPI) { - $accessToken = (Get-AzAccessToken).Token - $zipContents = Get-Item -Path $ZipFilePath - - $publishProfile = Get-AzWebAppPublishingProfile -Name $AppName -ResourceGroupName $ResourceGroupName - $zipUrl = ([System.uri]($publishProfile | Select-Xml -XPath "//publishProfile[@publishMethod='ZipDeploy']" | Select-Object -ExpandProperty Node).publishUrl).Scheme - } - do { - try { - if (-not $UseAPI) { - Publish-AzWebApp ` - -Name $AppName ` - -ResourceGroupName $ResourceGroupName ` - -ArchivePath $ZipFilePath ` - -Restart ` - -Force ` - | Out-Null - } - else { - Invoke-RestMethod ` - -Uri "https://${zipUrl}/api/zipdeploy" ` - -Method Post ` - -ContentType "multipart/form-data" ` - -Headers @{ "Authorization" = "Bearer $accessToken" } ` - -Form @{ file = $zipContents } ` - -StatusCodeVariable statusCode ` - | Out-Null - - if ($statusCode -ne 200) { - throw [System.Exception]::New("Error while uploading ZIP Deploy via Kudu API! ($statusCode)") - } - } - - $publishSuccess = $True - Write-Host "INFO: ZIP Deploy archive successfully uploaded" -ForegroundColor Green - } - catch { - if ($publishRetries -gt 0) { - Write-Host "WARNING: Problem while uploading ZIP Deploy archive! Retrying..." -ForegroundColor Yellow - $publishRetries-- + try { + if (-not $UseAPI) { + Write-Host "INFO: Using Publish-AzWebApp for ZIP deployment..." -ForegroundColor Green + Publish-AzWebApp ` + -Name $AppName ` + -ResourceGroupName $ResourceGroupName ` + -ArchivePath $ZipFilePath ` + -Restart ` + -Force ` + | Out-Null + } + else { + Write-Host "INFO: Using Kudu API for ZIP deployment..." -ForegroundColor Green + + # Get publishing credentials + $publishingCredentials = Invoke-AzResourceAction ` + -ResourceGroupName $ResourceGroupName ` + -ResourceType Microsoft.Web/sites/config ` + -ResourceName "$AppName/publishingcredentials" ` + -Action list ` + -ApiVersion 2018-02-01 ` + -Force + + $username = $publishingCredentials.Properties.publishingUserName + $password = $publishingCredentials.Properties.publishingPassword + + # Create credentials for Kudu + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username, $password))) + + # Upload via Kudu API + $kuduApiUrl = "https://$AppName.scm.azurewebsites.net/api/zipdeploy" + + $headers = @{ + Authorization = "Basic $base64AuthInfo" + } + + Write-Host "INFO: Uploading ZIP to Kudu API endpoint..." -ForegroundColor Green + $response = Invoke-RestMethod -Uri $kuduApiUrl -Method POST -InFile $ZipFilePath -Headers $headers -ContentType "application/zip" -TimeoutSec 1800 + + Write-Host "INFO: Kudu API response: $response" -ForegroundColor Green + } + + $publishSuccess = $True + Write-Host "INFO: ZIP Deploy archive successfully uploaded" -ForegroundColor Green } - else { - Write-Host "ERROR: Unable to upload ZIP Deploy archive!" -ForegroundColor Red - throw $_ + catch { + if ($publishRetries -gt 0) { + Write-Host "WARNING: Problem while uploading ZIP Deploy archive! Retrying..." -ForegroundColor Yellow + Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red + $publishRetries-- + Start-Sleep -Seconds 30 # Wait before retry + } + else { + Write-Host "ERROR: Unable to upload ZIP Deploy archive!" -ForegroundColor Red + throw $_ + } } - } } while ($publishSuccess -eq $False -and $publishRetries -ge 0) - } +} - Function Update-UIApplication { +Function Update-UIApplication { Param( - [Parameter(Mandatory = $true)] - [string]$UIAppId, - [Parameter(Mandatory = $true)] - [string]$Endpoint + [Parameter(Mandatory = $true)] + [string]$UIAppId, + [Parameter(Mandatory = $true)] + [string]$Endpoint ) Write-Host "INFO: Updating UI Application with SPA configuration" -ForegroundColor Green @@ -1055,340 +359,154 @@ process { Update-AzADApplication -ApplicationId $UIAppId -SPARedirectUri $appServiceEndpoint Write-Host "INFO: UI Application SPA configuration update complete" -ForegroundColor Green - } - - # Main Deployment Script Section - Write-Host - - if ($DEBUG_MODE) { - Write-Host "DEBUG: Debug Mode Enabled" -ForegroundColor Gray - } - - Write-Host "NOTE: IPAM Deployment Type: $($PSCmdlet.ParameterSetName)" -ForegroundColor Magenta - - try { - if ($PrivateAcr) { - Write-Host "INFO: PrivateACR flag set, verifying minimum Azure CLI version" -ForegroundColor Green - - # Verify Minimum Azure CLI Version - $azureCliVer = [System.Version](az version | ConvertFrom-Json).'azure-cli' - - if ($azureCliVer -lt $MIN_AZ_CLI_VER) { - Write-Host "ERROR: Azure CLI must be version $MIN_AZ_CLI_VER or greater!" -ForegroundColor Red - exit - } - - Write-Host "INFO: PrivateACR flag set, verifying Azure PowerShell and Azure CLI contexts match" -ForegroundColor Green - - # Verify Azure PowerShell and Azure CLI Contexts Match - $azureCliContext = $(az account show | ConvertFrom-Json) 2>$null - - if (-not $azureCliContext) { - Write-Host "ERROR: Azure CLI not logged in or no subscription has been selected!" -ForegroundColor Red - exit - } - - $azureCliSub = $azureCliContext.id - $azurePowerShellSub = (Get-AzContext).Subscription.Id - - if ($azurePowerShellSub -ne $azureCliSub) { - Write-Host "ERROR: Azure PowerShell and Azure CLI must be set to the same context!" -ForegroundColor Red - exit - } - } - - if ($PSCmdlet.ParameterSetName -in ('App', 'AppContainer', 'Function', 'FunctionContainer', 'AppsOnly')) { - # Fetch Tenant ID (If Required) - if ($MgmtGroupId) { - Write-Host "NOTE: Management Group ID Specified" -ForegroundColor Magenta - } - else { - Write-Host "INFO: Fetching Tenant ID from Azure PowerShell SDK" -ForegroundColor Green - $script:MgmtGroupId = (Get-AzContext).Tenant.Id - } - - Write-Host "INFO: Fetching Azure Cloud type from Azure PowerShell SDK" -ForegroundColor Green - - # Fetch Azure Cloud Type - $azureCloud = $AZURE_ENV_MAP[(Get-AzContext).Environment.Name] - - # Verify Azure Cloud Type is Supported - if (-not [bool]$azureCloud) { - Write-Host "ERROR: Azure Cloud type is not currently supported!" -ForegroundColor Red - Write-Host - Write-Host "Azure Cloud type: " -ForegroundColor Yellow -NoNewline - Write-Host (Get-AzContext).Environment.Name -ForegroundColor Cyan - exit - } - } - - if ($PSCmdlet.ParameterSetName -in ('App', 'AppContainer', 'Function', 'FunctionContainer')) { - Write-Host "INFO: Validating Azure Region selected for deployment" -ForegroundColor Green - - # Validate Azure Region - if (Test-Location -Location $Location) { - Write-Host "INFO: Azure Region validated successfully" -ForegroundColor Green - } - else { - Write-Host "ERROR: Location provided is not a valid Azure Region!" -ForegroundColor Red - Write-Host - Write-Host "Azure Region: " -ForegroundColor Yellow -NoNewline - Write-Host $Location -ForegroundColor Cyan - exit - } - } - - if (-not $ParameterFile) { - $appDetails = Deploy-IPAMApplications ` - -UIAppName $UIAppName ` - -EngineAppName $EngineAppName ` - -MgmtGroupId $MgmtGroupId ` - -AzureCloud $azureCloud ` - -DisableUI $DisableUI - - $consentDetails = @{ - EngineAppId = $appDetails.EngineAppId - } - - if (-not $DisableUI) { - $consentDetails.Add("UIAppId", $appDetails.UIAppId) - } - - Grant-AdminConsent @consentDetails -AzureCloud $azureCloud -DisableUI $DisableUI - } - - if ($PSCmdlet.ParameterSetName -in ('AppsOnly')) { - Save-Parameters @appDetails -DisableUI $DisableUI - } - - if ($ParameterFile) { - $appDetails = Import-Parameters ` - -ParameterFile $ParameterFile - } - - if ($PSCmdlet.ParameterSetName -in ('App', 'AppContainer', 'Function', 'FunctionContainer')) { - $deployment = Deploy-Bicep @appDetails ` - -NamePrefix $NamePrefix ` - -AzureCloud $azureCloud ` - -PrivateAcr $PrivateAcr ` - -Function $Function ` - -Native $Native ` - -Tags $Tags ` - -ResourceNames $ResourceNames - } - - if (($PSCmdlet.ParameterSetName -notin 'AppsOnly') -and (-not $DisableUI)) { - Update-UIApplication ` - -UIAppId $appDetails.UIAppId ` - -Endpoint $deployment.Outputs["appServiceHostName"].Value - } +} - if ($PSCmdlet.ParameterSetName -in ('App', 'Function')) { - if (-Not $ZipFilePath) { +# Main Deployment Script Section +Write-Host +Write-Host "INFO: Starting IPAM Deployment" -ForegroundColor Green + +try { + # Deploy the infrastructure + Write-Host "INFO: Deploying IPAM infrastructure..." -ForegroundColor Green + $deployment = Deploy-Bicep + + # Store engine app secret in Key Vault (after infrastructure is deployed) + if ($engineSecret -and $resourceNames.keyVaultName) { + Write-Host "" + Write-Host "=== KEY VAULT SECRET MANAGEMENT ===" -ForegroundColor Cyan + Write-Host "INFO: Storing engine app secret in Key Vault..." -ForegroundColor Green + Write-Host "INFO: Key Vault: $($resourceNames.keyVaultName)" -ForegroundColor White + Write-Host "INFO: Secret Name: ENGINE-SECRET" -ForegroundColor White + try { - # Create a temporary folder path - $TempFolder = Join-Path -Path TEMP:\ -ChildPath $(New-Guid) - - # Create directory if not exists - $script:TempFolderObj = New-Item -ItemType Directory -Path $TempFolder -Force + $secretStored = Set-KeyvaultSecret -KeyVaultName $resourceNames.keyVaultName -SecretName "ENGINE-SECRET" -SecretPlainTextValue $engineSecret + if ($secretStored) { + Write-Host "✓ Engine app secret stored securely in Key Vault" -ForegroundColor Green + + # Remove secret from build.json if it exists (for security) + if ($BUILD.ipam.engineAppSecret) { + Write-Host "INFO: Removing engine app secret from build.json for security..." -ForegroundColor Yellow + $BUILD.ipam.PSObject.Properties.Remove('engineAppSecret') + $BUILD | ConvertTo-Json -Depth 15 | Set-Content -Path $BuildConfigPath -Encoding UTF8 + Write-Host "✓ Engine app secret removed from build.json - now stored securely in Key Vault" -ForegroundColor Green + } + + Write-Host "" + Write-Host "⚠️ MANUAL ACTION REQUIRED: OPERATIONS TEAM ACCESS" -ForegroundColor Yellow + Write-Host "Please grant the Operations Team admin access to the Key Vault:" -ForegroundColor Yellow + Write-Host "" + Write-Host "STEPS:" -ForegroundColor Cyan + Write-Host "1. Go to Azure Portal > Key Vaults > $($resourceNames.keyVaultName)" -ForegroundColor Cyan + Write-Host "2. Click 'Access policies' or 'Access control (IAM)'" -ForegroundColor Cyan + Write-Host "3. Add the Operations Team with 'Key Vault Administrator' role" -ForegroundColor Cyan + Write-Host "4. This ensures the ops team can manage secrets for maintenance" -ForegroundColor Cyan + Write-Host "" + Write-Host "OR use Azure CLI:" -ForegroundColor Cyan + Write-Host "az keyvault set-policy --name $($resourceNames.keyVaultName) --object-id --secret-permissions all --key-permissions all --certificate-permissions all" -ForegroundColor Gray + Write-Host "" + } } catch { - Write-Host "ERROR: Unable to create temp directory to store ZIP archive!" -ForegroundColor Red - throw $_ + Write-Warning "Failed to store engine app secret in Key Vault: $_" + Write-Host "" + Write-Host "⚠️ MANUAL ACTION REQUIRED: KEY VAULT SECRET" -ForegroundColor Red + Write-Host "Deployment will continue, but you need to manually add the secret:" -ForegroundColor Yellow + Write-Host "" + Write-Host "STEPS:" -ForegroundColor Cyan + Write-Host "1. Go to Azure Portal > Key Vaults > $($resourceNames.keyVaultName)" -ForegroundColor Cyan + Write-Host "2. Click 'Secrets' > 'Generate/Import'" -ForegroundColor Cyan + Write-Host "3. Name: ENGINE-SECRET" -ForegroundColor Cyan + Write-Host "4. Value: [Your Engine App Secret]" -ForegroundColor Cyan + Write-Host "5. Grant Operations Team admin access (see above)" -ForegroundColor Cyan + Write-Host "" } - - Write-Host "INFO: Fetching latest ZIP Deploy archive..." -ForegroundColor Green - - Get-ZipFile -GitHubUserName $GitHubUserName -GitHubRepoName $GitHubRepoName -ZipFileName $ZipFileName -AssetFolder $TempFolderObj - - $script:ZipFilePath = Join-Path -Path $TempFolderObj.FullName -ChildPath $ZipFileName - } - else { - $script:ZipFilePath = Get-Item -Path $ZipFilePath - } - - Write-Host "INFO: Uploading ZIP Deploy archive..." -ForegroundColor Green - - try { - Publish-ZipFile -AppName $deployment.Outputs["appServiceName"].Value -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value -ZipFilePath $ZipFilePath - } - catch { - Write-Host "SWITCH: Retrying ZIP Deploy with Kudu API..." -ForegroundColor Blue - Publish-ZipFile -AppName $deployment.Outputs["appServiceName"].Value -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value -ZipFilePath $ZipFilePath -UseAPI - } - - if ($TempFolderObj) { - Write-Host "INFO: Cleaning up temporary directory" -ForegroundColor Green - Remove-Item -LiteralPath $TempFolderObj.FullName -Force -Recurse -ErrorAction SilentlyContinue - $script:TempFolderObj = $null - } } - if ($PSCmdlet.ParameterSetName -in ('AppContainer', 'FunctionContainer') -and $PrivateAcr) { - Write-Host "INFO: Building and pushing container image to Azure Container Registry" -ForegroundColor Green - - $containerMap = @{ - Debian = @{ - Extension = 'deb' - Port = 8080 - Images = @{ - Build = 'node:18-slim' - Serve = 'python:3.9-slim' - } - } - RHEL = @{ - Extension = 'rhel' - Port = 8080 - Images = @{ - Build = 'registry.access.redhat.com/ubi8/nodejs-18' - Serve = 'registry.access.redhat.com/ubi8/python-39' - } - } - } + # Handle ZIP deployment for native apps + if (-not $deployAsContainer) { + if (-not $ZipFilePath) { + try { + # Create a temporary folder path + $TempFolder = Join-Path -Path $env:TEMP -ChildPath $(New-Guid) - $dockerFile = 'Dockerfile.' + $containerMap[$ContainerType].Extension - $dockerFilePath = Join-Path -Path $ROOT_DIR -ChildPath $dockerFile - $dockerFileFunc = Join-Path -Path $ROOT_DIR -ChildPath 'Dockerfile.func' + # Create directory if not exists + $script:TempFolderObj = New-Item -ItemType Directory -Path $TempFolder -Force + } + catch { + Write-Host "ERROR: Unable to create temp directory to store ZIP archive!" -ForegroundColor Red + throw $_ + } - if ($Function) { - Write-Host "INFO: Building Function container..." -ForegroundColor Green + Write-Host "INFO: Fetching latest ZIP Deploy archive..." -ForegroundColor Green - $funcBuildOutput = $( - az acr build -r $deployment.Outputs["acrName"].Value ` - -t ipamfunc:latest ` - -f $dockerFileFunc $ROOT_DIR ` - --no-logs - ) *>&1 + $GitHubUserName = "Azure" + $GitHubRepoName = "ipam" + $ZipFileName = "ipam.zip" - if ($LASTEXITCODE -ne 0) { - Write-Host "ERROR: Container build process failed, fetching error logs..." -ForegroundColor Red + Get-ZipFile -GitHubUserName $GitHubUserName -GitHubRepoName $GitHubRepoName -ZipFileName $ZipFileName -AssetFolder $TempFolderObj - $buildId = [regex]::Matches($funcBuildOutput, "(?<=Queued a build with ID: )[\w]*").Value.Trim() - - $buildLogs = Get-BuildLogs ` - -SubscriptionId $deployment.Outputs["subscriptionId"].Value ` - -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value ` - -RegistryName $deployment.Outputs["acrName"].Value ` - -BuildId $buildId ` - -AzureCloud $azureCloud - - $buildLogs | Out-File -FilePath $errorLog -Append - - $script:containerBuildError = $true + $script:ZipFilePath = Join-Path -Path $TempFolderObj.FullName -ChildPath $ZipFileName } else { - Write-Host "INFO: Function container image build and push completed successfully" -ForegroundColor Green + $script:ZipFilePath = Get-Item -Path $ZipFilePath } - Write-Host "INFO: Restarting Function App" -ForegroundColor Green - - Restart-AzFunctionApp -Name $deployment.Outputs["appServiceName"].Value -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value -Force | Out-Null - } - else { - Write-Host "INFO: Building App container (" -ForegroundColor Green -NoNewline - Write-Host $ContainerType.ToLower() -ForegroundColor Cyan -NoNewline - Write-Host ")..." -ForegroundColor Green + Write-Host "INFO: Uploading ZIP Deploy archive..." -ForegroundColor Green - $appBuildOutput = $( - az acr build -r $deployment.Outputs["acrName"].Value ` - -t ipam:latest ` - -f $dockerFilePath $ROOT_DIR ` - --build-arg PORT=$($containerMap[$ContainerType].Port) ` - --build-arg BUILD_IMAGE=$($containerMap[$ContainerType].Images.Build) ` - --build-arg SERVE_IMAGE=$($containerMap[$ContainerType].Images.Serve) ` - --no-logs - ) *>&1 - - if ($LASTEXITCODE -ne 0) { - Write-Host "ERROR: Container build process failed, fetching error logs..." -ForegroundColor Red - - $buildId = [regex]::Matches($appBuildOutput, "(?<=Queued a build with ID: )[\w]*").Value.Trim() - - $buildLogs = Get-BuildLogs ` - -SubscriptionId $deployment.Outputs["subscriptionId"].Value ` - -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value ` - -RegistryName $deployment.Outputs["acrName"].Value ` - -BuildId $buildId ` - -AzureCloud $azureCloud - - $buildLogs | Out-File -FilePath $errorLog -Append - - $script:containerBuildError = $true + try { + $appServiceName = if ($deployAsFunc) { $deployment.Outputs["functionAppName"].Value } else { $deployment.Outputs["appServiceName"].Value } + + # Configure App Service for ZIP deployment + Write-Host "INFO: Configuring App Service for ZIP deployment..." -ForegroundColor Green + Set-AzWebApp -Name $appServiceName -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value -AppSettings @{ + "SCM_DO_BUILD_DURING_DEPLOYMENT" = "true" + "ENABLE_ORYX_BUILD" = "true" + "POST_BUILD_SCRIPT_PATH" = "" + } + + Publish-ZipFile -AppName $appServiceName -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value -ZipFilePath $ZipFilePath } - else { - Write-Host "INFO: App container image build and push completed successfully" -ForegroundColor Green + catch { + Write-Host "WARNING: Retrying ZIP Deploy with alternative method..." -ForegroundColor Yellow + Publish-ZipFile -AppName $appServiceName -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value -ZipFilePath $ZipFilePath -UseAPI } - if (-not $containerBuildError) { - Write-Host "INFO: Restarting App Service" -ForegroundColor Green - - Restart-AzWebApp -Name $deployment.Outputs["appServiceName"].Value -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value | Out-Null + if ($TempFolderObj) { + Write-Host "INFO: Cleaning up temporary directory" -ForegroundColor Green + Remove-Item -LiteralPath $TempFolderObj.FullName -Force -Recurse -ErrorAction SilentlyContinue + $script:TempFolderObj = $null } - } } - if (-not $containerBuildError) { - Write-Host "INFO: Azure IPAM Solution deployed successfully" -ForegroundColor Green - } - else { - Write-Host "WARNING: Azure IPAM Solution deployed with errors, see logs for details!" -ForegroundColor Yellow - Write-Host "Run Log: $transcriptLog" -ForegroundColor Yellow - Write-Host "Error Log: $errorLog" -ForegroundColor Yellow - } - - if ($($PSCmdlet.ParameterSetName -notin 'AppsOnly') -and (-not $DisableUI) -and $ParameterFile) { - $updateUrl = "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Authentication/appId/$($appDetails.UIAppId)" - $updateAddr = "https://$($deployment.Outputs["appServiceHostName"].Value)" - - Write-Host - Write-Host "POST DEPLOYMENT TASKS:" -ForegroundColor Yellow - Write-Host "##############################################" -ForegroundColor Yellow - Write-Host "Navigate In Browser To:" -ForegroundColor Cyan - Write-Host $updateUrl -ForegroundColor White - Write-Host "Change 'Redirect URI' To:" -ForegroundColor Cyan - Write-Host $updateAddr -ForegroundColor White - Write-Host "##############################################" -ForegroundColor Yellow - } - - if ($PSCmdlet.ParameterSetName -in ('App', 'Function')) { - Write-Host - Write-Host "NOTE: Please allow ~5 minutes for the Azure IPAM service to become available" -ForegroundColor Yellow - } - - $script:deploymentSuccess = $true - } - catch { - $_ | Out-File -FilePath $errorLog -Append - Write-Host "ERROR: Unable to deploy Azure IPAM solution due to an exception, see logs for detailed information!" -ForegroundColor Red - Write-Host "Run Log: $transcriptLog" -ForegroundColor Red - Write-Host "Error Log: $errorLog" -ForegroundColor Red - - if ($DEBUG_MODE) { - Write-Host "Debug Log: $debugLog" -ForegroundColor Red - } - - if ($env:CI) { - Write-Host $_.ToString() - } - - exit 1 - } - finally { - if ($TempFolderObj) { - Remove-Item -LiteralPath $TempFolderObj.FullName -Force -Recurse -ErrorAction SilentlyContinue - } + Write-Host "INFO: Azure IPAM Solution deployed successfully" -ForegroundColor Green - Write-Host - Stop-Transcript | Out-Null - - if (($PSCmdlet.ParameterSetName -notin 'AppsOnly') -and $script:deploymentSuccess) { - Write-Output "ipamURL=https://$($deployment.Outputs["appServiceHostName"].Value)" >> $env:GITHUB_OUTPUT - Write-Output "ipamUIAppId=$($appDetails.UIAppId)" >> $env:GITHUB_OUTPUT - Write-Output "ipamEngineAppId=$($appDetails.EngineAppId)" >> $env:GITHUB_OUTPUT - Write-Output "ipamSuffix=$($deployment.Outputs["suffix"].Value)" >> $env:GITHUB_OUTPUT - Write-Output "ipamResourceGroup=$($deployment.Outputs["resourceGroupName"].Value)" >> $env:GITHUB_OUTPUT + # Display post-deployment information + if (-not $BUILD.ipam.config.disableUI -and $BUILD.ipam.uiAppId) { + $appServiceHostName = $deployment.Outputs["appServiceHostName"].Value + + Write-Host + Write-Host "=== DEPLOYMENT COMPLETE ===" -ForegroundColor Green + Write-Host "IPAM UI URL: https://$appServiceHostName" -ForegroundColor Cyan + Write-Host "Engine App ID: $engineAppId" -ForegroundColor White + Write-Host "UI App ID: $($BUILD.ipam.uiAppId)" -ForegroundColor White + Write-Host "Key Vault: $($resourceNames.keyVaultName)" -ForegroundColor White + Write-Host "Resource Group: $($resourceNames.resourceGroupName)" -ForegroundColor White + Write-Host "" + Write-Host "=== POST-DEPLOYMENT CHECKLIST ===" -ForegroundColor Yellow + Write-Host "□ Grant Operations Team access to Key Vault (see instructions above)" -ForegroundColor Yellow + Write-Host "□ Verify IPAM UI is accessible at the URL above" -ForegroundColor Yellow + Write-Host "□ Test IPAM Engine API functionality" -ForegroundColor Yellow + Write-Host "□ Configure IPAM network discovery (if needed)" -ForegroundColor Yellow + Write-Host "" } - - exit - } } +catch { + Write-Host "ERROR: Azure IPAM Solution deployment failed!" -ForegroundColor Red + Write-Host "Error: $_" -ForegroundColor Red + Write-Host "Run Log: $transcriptLog" -ForegroundColor Yellow + Write-Host "Error Log: $errorLog" -ForegroundColor Yellow + throw $_ +} +finally { + Stop-Transcript +} \ No newline at end of file diff --git a/deploy/main.bicep b/deploy/main.bicep index 1e1c5dcf..129ac316 100644 --- a/deploy/main.bicep +++ b/deploy/main.bicep @@ -1,30 +1,15 @@ // Global parameters targetScope = 'subscription' -@description('GUID for Resource Naming') -param guid string = newGuid() - -@description('Deployment Location') -param location string = deployment().location - -@maxLength(7) -@description('Prefix for Resource Naming') -param namePrefix string = 'ipam' - -@description('Azure Cloud Enviroment') -param azureCloud string = 'AZURE_PUBLIC' +@description('Build configuration object') +#disable-next-line no-unused-params +param BUILD object -@description('Flag to Deploy Private Container Registry') -param privateAcr bool = false +@description('IPAM spoke configuration') +param ipamSpoke object -@description('Flag to Deploy IPAM as a Function') -param deployAsFunc bool = false - -@description('Flag to Deploy IPAM as a Container') -param deployAsContainer bool = false - -@description('IPAM-UI App Registration Client/App ID') -param uiAppId string = '00000000-0000-0000-0000-000000000000' +@description('IPAM configuration') +param ipamConfig object @description('IPAM-Engine App Registration Client/App ID') param engineAppId string @@ -33,30 +18,38 @@ param engineAppId string @description('IPAM-Engine App Registration Client Secret') param engineAppSecret string -@description('Tags') -param tags object = {} - -@description('IPAM Resource Names') -param resourceNames object = { - functionName: '${namePrefix}-${uniqueString(guid)}' - appServiceName: '${namePrefix}-${uniqueString(guid)}' - functionPlanName: '${namePrefix}-asp-${uniqueString(guid)}' - appServicePlanName: '${namePrefix}-asp-${uniqueString(guid)}' - cosmosAccountName: '${namePrefix}-dbacct-${uniqueString(guid)}' - cosmosContainerName: '${namePrefix}-ctr' - cosmosDatabaseName: '${namePrefix}-db' - keyVaultName: '${namePrefix}-kv-${uniqueString(guid)}' - workspaceName: '${namePrefix}-law-${uniqueString(guid)}' - managedIdentityName: '${namePrefix}-mi-${uniqueString(guid)}' - resourceGroupName: '${namePrefix}-rg-${uniqueString(guid)}' - storageAccountName: '${namePrefix}stg${uniqueString(guid)}' - containerRegistryName: '${namePrefix}acr${uniqueString(guid)}' +@description('GUID for Resource Naming') +param guid string = newGuid() + +// Use ipamConfig for deployment settings +var location = ipamConfig.location +var azureCloud = ipamConfig.azureCloud +var privateAcr = ipamConfig.privateAcr +var deployAsFunc = ipamConfig.deployAsFunc +var deployAsContainer = ipamConfig.deployAsContainer +var uiAppId = ipamConfig.uiAppId +var tags = ipamConfig.tags + +// Use ipamSpoke for resource names +var resourceNames = { + functionName: ipamSpoke.functionApp.name + appServiceName: ipamSpoke.appService.name + functionPlanName: ipamSpoke.appServicePlan.function + appServicePlanName: ipamSpoke.appServicePlan.app + cosmosAccountName: ipamSpoke.cosmosDb.accountName + cosmosContainerName: ipamSpoke.cosmosDb.containerName + cosmosDatabaseName: ipamSpoke.cosmosDb.databaseName + keyVaultName: ipamSpoke.keyVault.name + workspaceName: ipamSpoke.logAnalytics.name + managedIdentityName: ipamSpoke.managedIdentity.name + resourceGroupName: ipamSpoke.resourceGroup.name + storageAccountName: ipamSpoke.storageAccount.name + containerRegistryName: ipamSpoke.containerRegistry.name } // Resource Group resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { location: location - #disable-next-line use-stable-resource-identifiers name: resourceNames.resourceGroupName tags: tags } @@ -184,5 +177,6 @@ output subscriptionId string = subscription().subscriptionId output resourceGroupName string = resourceGroup.name output appServiceName string = deployAsFunc ? resourceNames.functionName : resourceNames.appServiceName output appServiceHostName string = deployAsFunc ? functionApp.outputs.functionAppHostName : appService.outputs.appServiceHostName +output appServiceUrl string = deployAsFunc ? 'https://${functionApp.outputs.functionAppHostName}' : 'https://${appService.outputs.appServiceHostName}' output acrName string = privateAcr ? containerRegistry.outputs.acrName : '' output acrUri string = privateAcr ? containerRegistry.outputs.acrUri : '' diff --git a/deploy/modules/appService.bicep b/deploy/modules/appService.bicep index ed245169..e5d1b2e3 100644 --- a/deploy/modules/appService.bicep +++ b/deploy/modules/appService.bicep @@ -84,7 +84,7 @@ resource appService 'Microsoft.Web/sites@2021-02-01' = { acrUserManagedIdentityID: privateAcr ? managedIdentityClientId : null alwaysOn: true linuxFxVersion: deployAsContainer ? 'DOCKER|${acrUri}/ipam:latest' : 'PYTHON|${pythonVersion}' - appCommandLine: !deployAsContainer ? 'bash ./init.sh 8000' : null + appCommandLine: deployAsContainer ? null : 'python -m uvicorn "app.main:app" --host "0.0.0.0" --port 8000' healthCheckPath: '/api/status' appSettings: concat( [