diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f14b3978b..a8348fd4d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -252,4 +252,23 @@ extends: GrafanaKeyVault: dotnet-grafana GrafanaVariableGroup: Dotnet-Grafana-Production ServiceConnectionClientId: fc1eb341-aea4-4a11-8f80-d14b8775b2ba - ServiceConnectionId: 4a511f6f-b538-48e6-a389-207e430634d1 \ No newline at end of file + ServiceConnectionId: 4a511f6f-b538-48e6-a389-207e430634d1 + + - template: /eng/deploy-managed-grafana.yml@self + parameters: + ${{ if ne(variables['Build.SourceBranch'], 'refs/heads/production') }}: + DeploymentEnvironment: Staging + ServiceConnectionName: dnceng-managed-grafana-staging + GrafanaWorkspaceName: dnceng-grafana-staging + GrafanaKeyVault: dnceng-grafana-int-kv + GrafanaVariableGroup: Dnceng-Managed-Grafana-Staging-Vg + ServiceConnectionClientId: 4ad9ae35-2d42-4245-a954-9003b7e31349 + ServiceConnectionId: f955b932-c7e3-48f7-9d67-4e6542b3568a + ${{ else }}: + DeploymentEnvironment: Production + ServiceConnectionName: dnceng-managed-grafana + GrafanaWorkspaceName: dnceng-grafana + GrafanaKeyVault: dnceng-grafana-prod-kv + GrafanaVariableGroup: Dnceng-Managed-Grafana-Vg + ServiceConnectionClientId: 0ceeca1a-31e7-49ee-9bf4-15f14ed28fa4 + ServiceConnectionId: 332b249e-769b-49a9-9dc9-d82afe28ec0a \ No newline at end of file diff --git a/eng/deploy-managed-grafana.yml b/eng/deploy-managed-grafana.yml new file mode 100644 index 000000000..15b7fd166 --- /dev/null +++ b/eng/deploy-managed-grafana.yml @@ -0,0 +1,32 @@ +parameters: +- name: ServiceConnectionName + type: string +- name: ServiceConnectionClientId + type: string +- name: ServiceConnectionId + type: string +- name: DeploymentEnvironment + type: string +- name: GrafanaWorkspaceName + type: string +- name: GrafanaKeyVault + type: string +- name: GrafanaVariableGroup + type: string + +stages: +- stage: ProvisionGrafana + displayName: 'Provision Grafana Infrastructure' + dependsOn: + - predeploy + - approval + jobs: + - template: /eng/provision-grafana.yaml@self + parameters: + DeploymentEnvironment: ${{ parameters.DeploymentEnvironment }} + ServiceConnectionName: ${{ parameters.ServiceConnectionName }} + GrafanaResourceGroup: 'monitoring-managed' + GrafanaWorkspaceName: ${{ parameters.GrafanaWorkspaceName }} + GrafanaLocation: 'westus2' + GrafanaKeyVault: ${{ parameters.GrafanaKeyVault }} + GrafanaVariableGroup: ${{ parameters.GrafanaVariableGroup }} \ No newline at end of file diff --git a/eng/deploy.yaml b/eng/deploy.yaml index efd8a04cf..3e18cd66a 100644 --- a/eng/deploy.yaml +++ b/eng/deploy.yaml @@ -156,6 +156,7 @@ stages: demands: ImageOverride -equals 1es-windows-2019 dependsOn: - deploy + - ProvisionGrafana variables: - group: ${{ parameters.StatusVariableGroup }} - group: ${{ parameters.GrafanaVariableGroup }} @@ -201,6 +202,7 @@ stages: demands: ImageOverride -equals 1es-windows-2019 dependsOn: - deploy + - ProvisionGrafana jobs: - job: scenario displayName: Scenario tests diff --git a/eng/deployment/azure-managed-grafana.bicep b/eng/deployment/azure-managed-grafana.bicep new file mode 100644 index 000000000..21d97e5ec --- /dev/null +++ b/eng/deployment/azure-managed-grafana.bicep @@ -0,0 +1,43 @@ +// Azure Managed Grafana Workspace Bicep Template +@description('The Azure region where the Grafana workspace will be deployed') +param location string + +@description('The name of the Grafana workspace') +param grafanaWorkspaceName string + +@description('The pricing tier for the Grafana workspace') +@allowed([ + 'Standard' + 'Essential' +]) +param skuName string = 'Standard' + +// Azure Managed Grafana Workspace +resource grafanaWorkspace 'Microsoft.Dashboard/grafana@2023-09-01' = { + name: grafanaWorkspaceName + location: location + sku: { + name: skuName + } + identity: { + type: 'SystemAssigned' + } + properties: { + deterministicOutboundIP: 'Enabled' + apiKey: 'Enabled' + autoGeneratedDomainNameLabelScope: 'TenantReuse' + zoneRedundancy: 'Disabled' + publicNetworkAccess: 'Enabled' + grafanaIntegrations: { + azureMonitorWorkspaceIntegrations: [] + } + } +} + +// Output the Grafana workspace details +output grafanaWorkspaceId string = grafanaWorkspace.id +output grafanaWorkspaceName string = grafanaWorkspace.name +output grafanaWorkspaceUrl string = grafanaWorkspace.properties.endpoint +output grafanaPrincipalId string = grafanaWorkspace.identity.principalId +output grafanaTenantId string = grafanaWorkspace.identity.tenantId +output grafanaWorkspaceLocation string = grafanaWorkspace.location diff --git a/eng/deployment/deploy-grafana.ps1 b/eng/deployment/deploy-grafana.ps1 new file mode 100644 index 000000000..f41b7efe8 --- /dev/null +++ b/eng/deployment/deploy-grafana.ps1 @@ -0,0 +1,141 @@ +# Azure Managed Grafana Deployment Script +# This script deploys an Azure Managed Grafana workspace using Bicep + +param( + [Parameter(Mandatory = $true)] + [string]$SubscriptionId, + + [Parameter(Mandatory = $true)] + [string]$ResourceGroupName, + + [Parameter(Mandatory = $true)] + [string]$Location, + + [Parameter(Mandatory = $true)] + [string]$GrafanaWorkspaceName, + + [Parameter(Mandatory = $false)] + [string]$DeploymentName = "grafana-deployment-$(Get-Date -Format 'yyyyMMdd-HHmmss')", + + [Parameter(Mandatory = $false)] + [switch]$WhatIf = $false +) + +# Set error action preference +$ErrorActionPreference = "Stop" + +Write-Host "=======================================" -ForegroundColor Cyan +Write-Host "Azure Managed Grafana Deployment Script" -ForegroundColor Cyan +Write-Host "=======================================" -ForegroundColor Cyan + +try { + # Check if Azure CLI is installed + Write-Host "Checking Azure CLI installation..." -ForegroundColor Yellow + az version 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Azure CLI is not installed or not in PATH. Please install Azure CLI first." + } + Write-Host "✓ Azure CLI is installed" -ForegroundColor Green + + # Check if user is logged in + Write-Host "Checking Azure authentication..." -ForegroundColor Yellow + $account = az account show 2>$null | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + Write-Host "Not logged in to Azure. Please login..." -ForegroundColor Yellow + az login + if ($LASTEXITCODE -ne 0) { + throw "Failed to login to Azure" + } + } + Write-Host "✓ Authenticated as: $($account.user.name)" -ForegroundColor Green + + # Set the subscription + Write-Host "Setting subscription to: $SubscriptionId" -ForegroundColor Yellow + az account set --subscription $SubscriptionId + if ($LASTEXITCODE -ne 0) { + throw "Failed to set subscription. Please check if the subscription ID is correct and you have access." + } + Write-Host "✓ Subscription set successfully" -ForegroundColor Green + + # Check if resource group exists, create if it doesn't + Write-Host "Checking if resource group '$ResourceGroupName' exists..." -ForegroundColor Yellow + az group show --name $ResourceGroupName 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "Resource group doesn't exist. Creating..." -ForegroundColor Yellow + az group create --name $ResourceGroupName --location $Location + if ($LASTEXITCODE -ne 0) { + throw "Failed to create resource group" + } + Write-Host "✓ Resource group created successfully" -ForegroundColor Green + } else { + Write-Host "✓ Resource group already exists" -ForegroundColor Green + } + + # Get the Bicep file path + $bicepFile = Join-Path $PSScriptRoot "azure-managed-grafana.bicep" + if (!(Test-Path $bicepFile)) { + throw "Bicep file not found at: $bicepFile" + } + Write-Host "✓ Bicep file found: $bicepFile" -ForegroundColor Green + + # Prepare deployment parameters + $parameters = @{ + location = $Location + grafanaWorkspaceName = $GrafanaWorkspaceName + skuName = "Standard" + } + + # Convert parameters to string format for Azure CLI + $paramString = ($parameters.GetEnumerator() | ForEach-Object { "$($_.Key)=`"$($_.Value)`"" }) -join " " + + # Run deployment + if ($WhatIf) { + Write-Host "Running what-if deployment..." -ForegroundColor Yellow + $cmd = "az deployment group what-if --resource-group $ResourceGroupName --template-file `"$bicepFile`" --parameters $paramString" + Write-Host "Command: $cmd" -ForegroundColor Gray + Invoke-Expression $cmd + } else { + Write-Host "Starting deployment..." -ForegroundColor Yellow + Write-Host "Deployment name: $DeploymentName" -ForegroundColor Gray + Write-Host "Resource group: $ResourceGroupName" -ForegroundColor Gray + Write-Host "Grafana workspace name: $GrafanaWorkspaceName" -ForegroundColor Gray + + $cmd = "az deployment group create --resource-group $ResourceGroupName --name $DeploymentName --template-file `"$bicepFile`" --parameters $paramString" + Write-Host "Command: $cmd" -ForegroundColor Gray + + $result = Invoke-Expression $cmd | ConvertFrom-Json + + if ($LASTEXITCODE -eq 0) { + Write-Host "=======================================" -ForegroundColor Green + Write-Host "✓ Deployment completed successfully!" -ForegroundColor Green + Write-Host "=======================================" -ForegroundColor Green + + # Display outputs + if ($result.properties.outputs) { + Write-Host "Deployment Outputs:" -ForegroundColor Cyan + $result.properties.outputs | ConvertTo-Json -Depth 3 | Write-Host + } + + # Get the Grafana workspace details + Write-Host "`nGrafana Workspace Details:" -ForegroundColor Cyan + $grafana = az grafana show --name $GrafanaWorkspaceName --resource-group $ResourceGroupName | ConvertFrom-Json + Write-Host "Workspace Name: $($grafana.name)" -ForegroundColor White + Write-Host "Workspace URL: $($grafana.properties.endpoint)" -ForegroundColor White + Write-Host "Location: $($grafana.location)" -ForegroundColor White + Write-Host "SKU: $($grafana.sku.name)" -ForegroundColor White + Write-Host "System Managed Identity: $($grafana.identity.principalId)" -ForegroundColor White + } else { + throw "Deployment failed" + } + } +} +catch { + Write-Host "=======================================" -ForegroundColor Red + Write-Host "❌ Error occurred during deployment:" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + Write-Host "=======================================" -ForegroundColor Red + exit 1 +} + +Write-Host "`n🎉 Script completed successfully!" -ForegroundColor Green +Write-Host "You can now access your Grafana workspace and configure it as needed." -ForegroundColor Yellow diff --git a/eng/provision-grafana.yaml b/eng/provision-grafana.yaml new file mode 100644 index 000000000..72faafe97 --- /dev/null +++ b/eng/provision-grafana.yaml @@ -0,0 +1,174 @@ +# Azure Managed Grafana Provisioning Template +# This template provisions Azure Managed Grafana workspaces as part of the deployment process + +parameters: +- name: DeploymentEnvironment + type: string + +- name: ServiceConnectionName + type: string + +- name: GrafanaResourceGroup + type: string + +- name: GrafanaWorkspaceName + type: string + +- name: GrafanaLocation + type: string + +- name: GrafanaKeyVault + type: string + +- name: GrafanaVariableGroup + type: string + +- name: SkipGrafanaProvisioning + type: boolean + default: false + +jobs: +- job: ProvisionGrafana + displayName: 'Provision Azure Managed Grafana' + pool: + name: NetCore1ESPool-Internal + demands: ImageOverride -equals 1es-windows-2022 + + variables: + - group: ${{ parameters.GrafanaVariableGroup }} + + steps: + - checkout: self + displayName: 'Checkout Repository' + + - task: AzureCLI@2 + displayName: 'Validate Bicep Template' + inputs: + azureSubscription: '${{ parameters.ServiceConnectionName }}' + scriptType: 'ps' + scriptLocation: 'inlineScript' + inlineScript: | + Write-Host "Validating Grafana Bicep template..." + if (!(Test-Path "eng/deployment/azure-managed-grafana.bicep")) { + throw "Bicep template not found: azure-managed-grafana.bicep" + } + + az bicep build --file eng/deployment/azure-managed-grafana.bicep + if ($LASTEXITCODE -ne 0) { + throw "Bicep template validation failed" + } + Write-Host "SUCCESS: Bicep template validation successful" + + - task: AzureCLI@2 + displayName: 'Ensure Resource Group Exists' + inputs: + azureSubscription: '${{ parameters.ServiceConnectionName }}' + scriptType: 'ps' + scriptLocation: 'inlineScript' + inlineScript: | + $rgName = "${{ parameters.GrafanaResourceGroup }}" + $location = "${{ parameters.GrafanaLocation }}" + + Write-Host "Checking if resource group '$rgName' exists..." + $rg = az group show --name $rgName --query "name" --output tsv 2>$null + + if ($LASTEXITCODE -ne 0) { + Write-Host "Creating resource group '$rgName' in '$location'..." + az group create --name $rgName --location $location + if ($LASTEXITCODE -ne 0) { + throw "Failed to create resource group '$rgName'" + } + Write-Host "SUCCESS: Resource group created successfully" + } else { + Write-Host "SUCCESS: Resource group already exists" + } + + - task: AzureResourceManagerTemplateDeployment@3 + displayName: 'Deploy Grafana Workspace' + inputs: + deploymentScope: 'Resource Group' + azureResourceManagerConnection: '${{ parameters.ServiceConnectionName }}' + action: 'Create Or Update Resource Group' + resourceGroupName: '${{ parameters.GrafanaResourceGroup }}' + location: '${{ parameters.GrafanaLocation }}' + templateLocation: 'Linked artifact' + csmFile: 'eng/deployment/azure-managed-grafana.bicep' + overrideParameters: '-location "${{ parameters.GrafanaLocation }}" -grafanaWorkspaceName "${{ parameters.GrafanaWorkspaceName }}" -skuName "Standard"' + deploymentMode: 'Incremental' + deploymentName: 'grafana-${{ parameters.DeploymentEnvironment }}-$(Build.BuildNumber)' + deploymentOutputs: 'grafanaOutputs' + + - task: AzureCLI@2 + displayName: 'Install Azure Managed Grafana Extension' + inputs: + azureSubscription: '${{ parameters.ServiceConnectionName }}' + scriptType: 'ps' + scriptLocation: 'inlineScript' + inlineScript: | + Write-Host "Installing Azure CLI Azure Managed Grafana extension..." + az extension add --name amg + if ($LASTEXITCODE -ne 0) { + Write-Host "Warning: Failed to install amg extension, will use alternative verification method" + } else { + Write-Host "SUCCESS: Azure Managed Grafana extension installed" + } + + - task: AzureCLI@2 + displayName: 'Verify Grafana Deployment' + inputs: + azureSubscription: '${{ parameters.ServiceConnectionName }}' + scriptType: 'ps' + scriptLocation: 'inlineScript' + inlineScript: | + $workspaceName = "${{ parameters.GrafanaWorkspaceName }}" + $rgName = "${{ parameters.GrafanaResourceGroup }}" + + Write-Host "Verifying Grafana workspace deployment..." + + # Wait for deployment to complete + $maxAttempts = 30 + $attempt = 0 + do { + $attempt++ + Write-Host "Verification attempt $attempt of $maxAttempts..." + + $workspace = az grafana show --name $workspaceName --resource-group $rgName 2>$null | ConvertFrom-Json + if ($workspace -and $workspace.properties.provisioningState -eq "Succeeded") { + break + } + + if ($attempt -lt $maxAttempts) { + Write-Host "Workspace not ready yet, waiting 30 seconds..." + Start-Sleep -Seconds 30 + } + } while ($attempt -lt $maxAttempts) + + if (!$workspace) { + throw "Failed to verify Grafana workspace deployment" + } + + Write-Host "GRAFANA WORKSPACE DETAILS:" + Write-Host " Name: $($workspace.name)" + Write-Host " URL: $($workspace.properties.endpoint)" + Write-Host " Location: $($workspace.location)" + Write-Host " SKU: $($workspace.sku.name)" + Write-Host " Status: $($workspace.properties.provisioningState)" + Write-Host " Identity: $($workspace.identity.principalId)" + + # Verify role assignments + Write-Host "Checking role assignments..." + $roleAssignments = az role assignment list --scope $workspace.id --query '[].{principalId:principalId, roleDefinitionName:roleDefinitionName}' 2>$null | ConvertFrom-Json + if ($roleAssignments) { + $roleAssignments | ForEach-Object { + Write-Host " Role: $($_.roleDefinitionName) - Principal: $($_.principalId)" + } + } else { + Write-Host " No role assignments found" + } + + # Store outputs for downstream usage + Write-Host "##vso[task.setvariable variable=GrafanaUrl;isOutput=true]$($workspace.properties.endpoint)" + Write-Host "##vso[task.setvariable variable=GrafanaPrincipalId;isOutput=true]$($workspace.identity.principalId)" + Write-Host "##vso[task.setvariable variable=GrafanaResourceId;isOutput=true]$($workspace.id)" + + Write-Host "SUCCESS: ${{ parameters.DeploymentEnvironment }} Grafana deployment verification completed" \ No newline at end of file