Compare commits
7 Commits
c331540369
...
42a04e6cb3
| Author | SHA1 | Date | |
|---|---|---|---|
| 42a04e6cb3 | |||
| 52f851c1f3 | |||
| 3c11402e6b | |||
| e1ad0d9aa7 | |||
| cb418ed051 | |||
| 9ad4bd9a73 | |||
| 6bb5907bbd |
133
azure-pipelines-deploy.yml
Normal file
133
azure-pipelines-deploy.yml
Normal file
@@ -0,0 +1,133 @@
|
||||
# Azure DevOps Pipeline - Deploy to Azure App Service
|
||||
# Use this pipeline after images have been built and pushed to ACR
|
||||
#
|
||||
# To use this pipeline:
|
||||
# 1. Make sure images exist in ACR (run azure-pipelines.yml first)
|
||||
# 2. Update variables below with your Azure resource names
|
||||
# 3. Create Azure service connection for App Service deployment
|
||||
# 4. Create 'production' environment in Azure DevOps
|
||||
# 5. Configure this pipeline in Azure DevOps
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
tags:
|
||||
include:
|
||||
- 'v*'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
# Azure Container Registry configuratie
|
||||
acrName: 'zdlas' # Pas aan naar jouw ACR naam
|
||||
repositoryName: 'cmdb-insight'
|
||||
|
||||
# Azure App Service configuratie
|
||||
resourceGroup: 'rg-cmdb-insight-prod' # Pas aan naar jouw resource group
|
||||
backendAppName: 'cmdb-backend-prod' # Pas aan naar jouw backend app naam
|
||||
frontendAppName: 'cmdb-frontend-prod' # Pas aan naar jouw frontend app naam
|
||||
azureSubscription: 'zuyderland-cmdb-subscription' # Azure service connection voor App Service deployment
|
||||
|
||||
# Deployment configuratie
|
||||
imageTag: 'latest' # Use 'latest' or specific tag like 'v1.0.0'
|
||||
|
||||
stages:
|
||||
- stage: Deploy
|
||||
displayName: 'Deploy to Azure App Service'
|
||||
jobs:
|
||||
- deployment: DeployBackend
|
||||
displayName: 'Deploy Backend'
|
||||
environment: 'production'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebAppContainer@1
|
||||
displayName: 'Deploy Backend Container'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
appName: '$(backendAppName)'
|
||||
containers: '$(acrName).azurecr.io/$(repositoryName)/backend:$(imageTag)'
|
||||
deployToSlotOrASE: false
|
||||
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Restart Backend App Service'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
echo "Restarting backend app service..."
|
||||
az webapp restart \
|
||||
--name $(backendAppName) \
|
||||
--resource-group $(resourceGroup)
|
||||
echo "Backend app service restarted successfully"
|
||||
|
||||
- deployment: DeployFrontend
|
||||
displayName: 'Deploy Frontend'
|
||||
environment: 'production'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebAppContainer@1
|
||||
displayName: 'Deploy Frontend Container'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
appName: '$(frontendAppName)'
|
||||
containers: '$(acrName).azurecr.io/$(repositoryName)/frontend:$(imageTag)'
|
||||
deployToSlotOrASE: false
|
||||
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Restart Frontend App Service'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
echo "Restarting frontend app service..."
|
||||
az webapp restart \
|
||||
--name $(frontendAppName) \
|
||||
--resource-group $(resourceGroup)
|
||||
echo "Frontend app service restarted successfully"
|
||||
|
||||
- job: VerifyDeployment
|
||||
displayName: 'Verify Deployment'
|
||||
dependsOn:
|
||||
- DeployBackend
|
||||
- DeployFrontend
|
||||
steps:
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Health Check'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
echo "Checking backend health..."
|
||||
BACKEND_URL="https://$(backendAppName).azurewebsites.net/api/health"
|
||||
FRONTEND_URL="https://$(frontendAppName).azurewebsites.net"
|
||||
|
||||
echo "Backend URL: $BACKEND_URL"
|
||||
echo "Frontend URL: $FRONTEND_URL"
|
||||
|
||||
# Wait a bit for apps to start
|
||||
sleep 10
|
||||
|
||||
# Check backend health
|
||||
BACKEND_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $BACKEND_URL || echo "000")
|
||||
if [ "$BACKEND_STATUS" = "200" ]; then
|
||||
echo "✅ Backend health check passed"
|
||||
else
|
||||
echo "⚠️ Backend health check returned status: $BACKEND_STATUS"
|
||||
fi
|
||||
|
||||
# Check frontend
|
||||
FRONTEND_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $FRONTEND_URL || echo "000")
|
||||
if [ "$FRONTEND_STATUS" = "200" ]; then
|
||||
echo "✅ Frontend is accessible"
|
||||
else
|
||||
echo "⚠️ Frontend returned status: $FRONTEND_STATUS"
|
||||
fi
|
||||
247
azure-pipelines-slots.yml
Normal file
247
azure-pipelines-slots.yml
Normal file
@@ -0,0 +1,247 @@
|
||||
# Azure DevOps Pipeline - Build, Push and Deploy with Deployment Slots
|
||||
# Advanced version with zero-downtime deployment using staging slots
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
tags:
|
||||
include:
|
||||
- 'v*'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
# Azure Container Registry configuratie
|
||||
acrName: 'zdlas' # Pas aan naar jouw ACR naam
|
||||
repositoryName: 'cmdb-insight'
|
||||
dockerRegistryServiceConnection: 'zuyderland-cmdb-acr-connection'
|
||||
|
||||
# Azure App Service configuratie
|
||||
resourceGroup: 'rg-cmdb-insight-prod'
|
||||
backendAppName: 'cmdb-backend-prod'
|
||||
frontendAppName: 'cmdb-frontend-prod'
|
||||
azureSubscription: 'zuyderland-cmdb-subscription'
|
||||
|
||||
# Deployment configuratie
|
||||
imageTag: '$(Build.BuildId)'
|
||||
deployToProduction: true
|
||||
useDeploymentSlots: true # Enable deployment slots
|
||||
stagingSlotName: 'staging'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: 'Build and Push Docker Images'
|
||||
jobs:
|
||||
- job: BuildImages
|
||||
displayName: 'Build Docker Images'
|
||||
steps:
|
||||
- task: Docker@2
|
||||
displayName: 'Build and Push Backend Image'
|
||||
inputs:
|
||||
command: buildAndPush
|
||||
repository: '$(repositoryName)/backend'
|
||||
dockerfile: 'backend/Dockerfile.prod'
|
||||
containerRegistry: '$(dockerRegistryServiceConnection)'
|
||||
tags: |
|
||||
$(imageTag)
|
||||
latest
|
||||
|
||||
- task: Docker@2
|
||||
displayName: 'Build and Push Frontend Image'
|
||||
inputs:
|
||||
command: buildAndPush
|
||||
repository: '$(repositoryName)/frontend'
|
||||
dockerfile: 'frontend/Dockerfile.prod'
|
||||
containerRegistry: '$(dockerRegistryServiceConnection)'
|
||||
tags: |
|
||||
$(imageTag)
|
||||
latest
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Output Image URLs'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
$backendImage = "$(acrName).azurecr.io/$(repositoryName)/backend:$(imageTag)"
|
||||
$frontendImage = "$(acrName).azurecr.io/$(repositoryName)/frontend:$(imageTag)"
|
||||
Write-Host "##vso[task.setvariable variable=backendImage;isOutput=true]$backendImage"
|
||||
Write-Host "##vso[task.setvariable variable=frontendImage;isOutput=true]$frontendImage"
|
||||
Write-Host "Backend Image: $backendImage"
|
||||
Write-Host "Frontend Image: $frontendImage"
|
||||
|
||||
- stage: DeployToStaging
|
||||
displayName: 'Deploy to Staging Slot'
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), eq(variables['deployToProduction'], true), eq(variables['useDeploymentSlots'], true))
|
||||
jobs:
|
||||
- deployment: DeployBackendStaging
|
||||
displayName: 'Deploy Backend to Staging'
|
||||
environment: 'staging'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebAppContainer@1
|
||||
displayName: 'Deploy Backend to Staging Slot'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
appName: '$(backendAppName)'
|
||||
deployToSlotOrASE: true
|
||||
resourceGroupName: '$(resourceGroup)'
|
||||
slotName: '$(stagingSlotName)'
|
||||
containers: '$(acrName).azurecr.io/$(repositoryName)/backend:latest'
|
||||
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Wait for Backend Staging to be Ready'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
echo "Waiting for backend staging to be ready..."
|
||||
sleep 30
|
||||
STAGING_URL="https://$(backendAppName)-$(stagingSlotName).azurewebsites.net/api/health"
|
||||
for i in {1..10}; do
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" $STAGING_URL || echo "000")
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "✅ Backend staging is ready"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting... ($i/10)"
|
||||
sleep 10
|
||||
done
|
||||
echo "⚠️ Backend staging health check timeout"
|
||||
|
||||
- deployment: DeployFrontendStaging
|
||||
displayName: 'Deploy Frontend to Staging'
|
||||
environment: 'staging'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebAppContainer@1
|
||||
displayName: 'Deploy Frontend to Staging Slot'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
appName: '$(frontendAppName)'
|
||||
deployToSlotOrASE: true
|
||||
resourceGroupName: '$(resourceGroup)'
|
||||
slotName: '$(stagingSlotName)'
|
||||
containers: '$(acrName).azurecr.io/$(repositoryName)/frontend:latest'
|
||||
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Wait for Frontend Staging to be Ready'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
echo "Waiting for frontend staging to be ready..."
|
||||
sleep 20
|
||||
STAGING_URL="https://$(frontendAppName)-$(stagingSlotName).azurewebsites.net"
|
||||
for i in {1..10}; do
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" $STAGING_URL || echo "000")
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "✅ Frontend staging is ready"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting... ($i/10)"
|
||||
sleep 10
|
||||
done
|
||||
echo "⚠️ Frontend staging health check timeout"
|
||||
|
||||
- stage: SwapToProduction
|
||||
displayName: 'Swap Staging to Production'
|
||||
dependsOn: DeployToStaging
|
||||
condition: and(succeeded(), eq(variables['deployToProduction'], true), eq(variables['useDeploymentSlots'], true))
|
||||
jobs:
|
||||
- deployment: SwapBackend
|
||||
displayName: 'Swap Backend to Production'
|
||||
environment: 'production'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Swap Backend Staging to Production'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
echo "Swapping backend staging to production..."
|
||||
az webapp deployment slot swap \
|
||||
--name $(backendAppName) \
|
||||
--resource-group $(resourceGroup) \
|
||||
--slot $(stagingSlotName) \
|
||||
--target-slot production
|
||||
echo "✅ Backend swapped to production"
|
||||
|
||||
- deployment: SwapFrontend
|
||||
displayName: 'Swap Frontend to Production'
|
||||
environment: 'production'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Swap Frontend Staging to Production'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
echo "Swapping frontend staging to production..."
|
||||
az webapp deployment slot swap \
|
||||
--name $(frontendAppName) \
|
||||
--resource-group $(resourceGroup) \
|
||||
--slot $(stagingSlotName) \
|
||||
--target-slot production
|
||||
echo "✅ Frontend swapped to production"
|
||||
|
||||
- stage: VerifyProduction
|
||||
displayName: 'Verify Production Deployment'
|
||||
dependsOn: SwapToProduction
|
||||
condition: and(succeeded(), eq(variables['deployToProduction'], true), eq(variables['useDeploymentSlots'], true))
|
||||
jobs:
|
||||
- job: VerifyDeployment
|
||||
displayName: 'Verify Production'
|
||||
steps:
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Production Health Check'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
echo "Verifying production deployment..."
|
||||
BACKEND_URL="https://$(backendAppName).azurewebsites.net/api/health"
|
||||
FRONTEND_URL="https://$(frontendAppName).azurewebsites.net"
|
||||
|
||||
echo "Backend URL: $BACKEND_URL"
|
||||
echo "Frontend URL: $FRONTEND_URL"
|
||||
|
||||
# Wait for swap to complete
|
||||
sleep 15
|
||||
|
||||
# Check backend health
|
||||
BACKEND_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $BACKEND_URL || echo "000")
|
||||
if [ "$BACKEND_STATUS" = "200" ]; then
|
||||
echo "✅ Backend production health check passed"
|
||||
else
|
||||
echo "❌ Backend production health check failed: $BACKEND_STATUS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check frontend
|
||||
FRONTEND_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $FRONTEND_URL || echo "000")
|
||||
if [ "$FRONTEND_STATUS" = "200" ]; then
|
||||
echo "✅ Frontend production is accessible"
|
||||
else
|
||||
echo "❌ Frontend production check failed: $FRONTEND_STATUS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 Production deployment verified successfully!"
|
||||
@@ -157,7 +157,11 @@ export class DebugController {
|
||||
*/
|
||||
async getObjectTypeStats(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const typeName = req.params.typeName;
|
||||
const typeName = Array.isArray(req.params.typeName) ? req.params.typeName[0] : req.params.typeName;
|
||||
if (!typeName) {
|
||||
res.status(400).json({ error: 'typeName parameter required' });
|
||||
return;
|
||||
}
|
||||
const services = getServices();
|
||||
|
||||
// Get object count
|
||||
@@ -194,7 +198,7 @@ export class DebugController {
|
||||
async getAllObjectTypes(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const services = getServices();
|
||||
const db = services.schemaRepo.db;
|
||||
const db = services.schemaRepo.getDatabaseAdapter();
|
||||
|
||||
// Check if object_types table exists
|
||||
try {
|
||||
@@ -243,7 +247,13 @@ export class DebugController {
|
||||
// Get enabled types via service (may fail if table has issues)
|
||||
let enabledTypes: Array<{ typeName: string; displayName: string; schemaId: string; objectTypeId: number }> = [];
|
||||
try {
|
||||
enabledTypes = await services.schemaSyncService.getEnabledObjectTypes();
|
||||
const rawTypes = await services.schemaRepo.getEnabledObjectTypes();
|
||||
enabledTypes = rawTypes.map(t => ({
|
||||
typeName: t.typeName,
|
||||
displayName: t.displayName,
|
||||
schemaId: t.schemaId.toString(),
|
||||
objectTypeId: t.id,
|
||||
}));
|
||||
logger.debug(`DebugController: getEnabledObjectTypes returned ${enabledTypes.length} types: ${enabledTypes.map(t => t.typeName).join(', ')}`);
|
||||
} catch (error) {
|
||||
logger.error('DebugController: Failed to get enabled types via service', error);
|
||||
@@ -295,9 +305,13 @@ export class DebugController {
|
||||
*/
|
||||
async diagnoseObjectType(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const typeName = req.params.typeName;
|
||||
const typeName = Array.isArray(req.params.typeName) ? req.params.typeName[0] : req.params.typeName;
|
||||
if (!typeName) {
|
||||
res.status(400).json({ error: 'typeName parameter required' });
|
||||
return;
|
||||
}
|
||||
const services = getServices();
|
||||
const db = services.schemaRepo.db;
|
||||
const db = services.schemaRepo.getDatabaseAdapter();
|
||||
const isPostgres = db.isPostgres === true;
|
||||
const enabledCondition = isPostgres ? 'enabled IS true' : 'enabled = 1';
|
||||
|
||||
@@ -351,14 +365,14 @@ export class DebugController {
|
||||
// Check enabled types via service
|
||||
let enabledTypesFromService: string[] = [];
|
||||
try {
|
||||
const enabledTypes = await services.schemaSyncService.getEnabledObjectTypes();
|
||||
enabledTypesFromService = enabledTypes.map(t => t.typeName);
|
||||
const rawTypes = await services.schemaRepo.getEnabledObjectTypes();
|
||||
enabledTypesFromService = rawTypes.map((t: { typeName: string }) => t.typeName);
|
||||
} catch (error) {
|
||||
logger.error('DebugController: Failed to get enabled types from service', error);
|
||||
}
|
||||
|
||||
// Check if type is in enabled list from service
|
||||
const isInEnabledList = enabledTypesFromService.includes(typeName);
|
||||
const isInEnabledList = enabledTypesFromService.includes(typeName as string);
|
||||
|
||||
res.json({
|
||||
requestedType: typeName,
|
||||
@@ -432,7 +446,7 @@ export class DebugController {
|
||||
async fixMissingTypeNames(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const services = getServices();
|
||||
const db = services.schemaRepo.db;
|
||||
const db = services.schemaRepo.getDatabaseAdapter();
|
||||
|
||||
// Find all object types with NULL or empty type_name
|
||||
// Also check for enabled ones specifically
|
||||
@@ -510,15 +524,16 @@ export class DebugController {
|
||||
}
|
||||
|
||||
// Re-fetch enabled types to verify fix (reuse services from line 294)
|
||||
const enabledTypesAfterFix = await services.schemaSyncService.getEnabledObjectTypes();
|
||||
const rawTypes = await services.schemaRepo.getEnabledObjectTypes();
|
||||
const enabledTypesAfterFix = rawTypes.map(t => t.typeName);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
fixed: fixes.length,
|
||||
errors: errors.length,
|
||||
errorCount: errors.length,
|
||||
fixes,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
enabledTypesAfterFix: enabledTypesAfterFix.map(t => t.typeName),
|
||||
enabledTypesAfterFix: enabledTypesAfterFix,
|
||||
note: enabledWithNullTypeName.length > 0
|
||||
? `Fixed ${enabledWithNullTypeName.length} enabled types that were missing type_name. They should now appear in enabled types list.`
|
||||
: undefined,
|
||||
|
||||
@@ -14,11 +14,11 @@ export class SyncController {
|
||||
async syncSchemas(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const services = getServices();
|
||||
const result = await services.schemaSyncService.syncAllSchemas();
|
||||
const result = await services.schemaSyncService.syncAll();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
...result,
|
||||
success: result.success !== undefined ? result.success : true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('SyncController: Failed to sync schemas', error);
|
||||
@@ -38,9 +38,9 @@ export class SyncController {
|
||||
const services = getServices();
|
||||
|
||||
// Get enabled types
|
||||
const enabledTypes = await services.schemaSyncService.getEnabledObjectTypes();
|
||||
const rawTypes = await services.schemaRepo.getEnabledObjectTypes();
|
||||
|
||||
if (enabledTypes.length === 0) {
|
||||
if (rawTypes.length === 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'No object types enabled for syncing. Please configure object types in Schema Configuration.',
|
||||
@@ -54,10 +54,10 @@ export class SyncController {
|
||||
let totalRelations = 0;
|
||||
|
||||
// Sync each enabled type
|
||||
for (const type of enabledTypes) {
|
||||
for (const type of rawTypes) {
|
||||
const result = await services.objectSyncService.syncObjectType(
|
||||
type.schemaId,
|
||||
type.objectTypeId,
|
||||
type.schemaId.toString(),
|
||||
type.id,
|
||||
type.typeName,
|
||||
type.displayName
|
||||
);
|
||||
@@ -95,21 +95,31 @@ export class SyncController {
|
||||
*/
|
||||
async syncObjectType(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const typeName = req.params.typeName;
|
||||
const typeName = Array.isArray(req.params.typeName) ? req.params.typeName[0] : req.params.typeName;
|
||||
if (!typeName) {
|
||||
res.status(400).json({ error: 'typeName parameter required' });
|
||||
return;
|
||||
}
|
||||
const services = getServices();
|
||||
|
||||
// Get enabled types
|
||||
let enabledTypes = await services.schemaSyncService.getEnabledObjectTypes();
|
||||
let rawTypes = await services.schemaRepo.getEnabledObjectTypes();
|
||||
let enabledTypes = rawTypes.map(t => ({
|
||||
typeName: t.typeName,
|
||||
displayName: t.displayName,
|
||||
schemaId: t.schemaId.toString(),
|
||||
objectTypeId: t.id,
|
||||
}));
|
||||
|
||||
// Filter out any entries with missing typeName
|
||||
enabledTypes = enabledTypes.filter(t => t && t.typeName);
|
||||
enabledTypes = enabledTypes.filter((t: { typeName?: string }) => t && t.typeName);
|
||||
|
||||
// Debug logging - also check database directly
|
||||
logger.info(`SyncController: Looking for type "${typeName}" in ${enabledTypes.length} enabled types`);
|
||||
logger.debug(`SyncController: Enabled types: ${JSON.stringify(enabledTypes.map(t => ({ typeName: t?.typeName, displayName: t?.displayName })))}`);
|
||||
logger.debug(`SyncController: Enabled types: ${JSON.stringify(enabledTypes.map((t: { typeName?: string; displayName?: string }) => ({ typeName: t?.typeName, displayName: t?.displayName })))}`);
|
||||
|
||||
// Additional debug: Check database directly for enabled types (including those with missing type_name)
|
||||
const db = services.schemaRepo.db;
|
||||
const db = services.schemaRepo.getDatabaseAdapter();
|
||||
const isPostgres = db.isPostgres === true;
|
||||
const enabledCondition = isPostgres ? 'enabled IS true' : 'enabled = 1';
|
||||
const dbCheck = await db.query<{ type_name: string | null; display_name: string; enabled: boolean | number; id: number; jira_type_id: number }>(
|
||||
@@ -119,9 +129,10 @@ export class SyncController {
|
||||
logger.debug(`SyncController: Database enabled types (raw): ${JSON.stringify(dbCheck.map(t => ({ id: t.id, displayName: t.display_name, typeName: t.type_name, hasTypeName: !!(t.type_name && t.type_name.trim() !== '') })))}`);
|
||||
|
||||
// Check if AzureSubscription or similar is enabled but missing type_name
|
||||
const matchingByDisplayName = dbCheck.filter(t =>
|
||||
t.display_name.toLowerCase().includes(typeName.toLowerCase()) ||
|
||||
typeName.toLowerCase().includes(t.display_name.toLowerCase())
|
||||
const typeNameLower = typeName.toLowerCase();
|
||||
const matchingByDisplayName = dbCheck.filter((t: { display_name: string }) =>
|
||||
t.display_name.toLowerCase().includes(typeNameLower) ||
|
||||
typeNameLower.includes(t.display_name.toLowerCase())
|
||||
);
|
||||
if (matchingByDisplayName.length > 0) {
|
||||
logger.warn(`SyncController: Found enabled type(s) matching "${typeName}" by display_name but not in enabled list:`, {
|
||||
@@ -135,7 +146,7 @@ export class SyncController {
|
||||
});
|
||||
}
|
||||
|
||||
const type = enabledTypes.find(t => t && t.typeName === typeName);
|
||||
const type = enabledTypes.find((t: { typeName?: string }) => t && t.typeName === typeName);
|
||||
|
||||
if (!type) {
|
||||
// Check if type exists but is not enabled or has missing type_name
|
||||
@@ -148,12 +159,13 @@ export class SyncController {
|
||||
logger.debug(`SyncController: Enabled types details: ${JSON.stringify(enabledTypes)}`);
|
||||
|
||||
// Try to find it with different case (handle undefined typeName)
|
||||
const caseInsensitiveMatch = enabledTypes.find(t => t && t.typeName && t.typeName.toLowerCase() === typeName.toLowerCase());
|
||||
const typeNameLower = typeName.toLowerCase();
|
||||
const caseInsensitiveMatch = enabledTypes.find((t: { typeName?: string }) => t && t.typeName && t.typeName.toLowerCase() === typeNameLower);
|
||||
if (caseInsensitiveMatch) {
|
||||
logger.warn(`SyncController: Found type with different case: "${caseInsensitiveMatch.typeName}" vs "${typeName}"`);
|
||||
// Use the found type with correct case
|
||||
const result = await services.objectSyncService.syncObjectType(
|
||||
caseInsensitiveMatch.schemaId,
|
||||
caseInsensitiveMatch.schemaId.toString(),
|
||||
caseInsensitiveMatch.objectTypeId,
|
||||
caseInsensitiveMatch.typeName,
|
||||
caseInsensitiveMatch.displayName
|
||||
@@ -168,7 +180,7 @@ export class SyncController {
|
||||
}
|
||||
|
||||
// Direct SQL query to verify enabled status and type_name
|
||||
const db = services.schemaRepo.db;
|
||||
const db = services.schemaRepo.getDatabaseAdapter();
|
||||
const isPostgres = db.isPostgres === true;
|
||||
const rawCheck = await db.queryOne<{ enabled: boolean | number; type_name: string | null; display_name: string }>(
|
||||
`SELECT enabled, type_name, display_name FROM object_types WHERE type_name = ?`,
|
||||
@@ -183,7 +195,7 @@ export class SyncController {
|
||||
);
|
||||
|
||||
// Get list of all enabled type names for better error message
|
||||
const enabledTypeNames = enabledTypes.map(t => t.typeName).filter(Boolean);
|
||||
const enabledTypeNames = enabledTypes.map((t: { typeName?: string }) => t.typeName).filter(Boolean) as string[];
|
||||
|
||||
// Check if the issue is that the type is enabled but has a missing type_name
|
||||
if (rawCheck && (rawCheck.enabled === true || rawCheck.enabled === 1)) {
|
||||
@@ -221,7 +233,7 @@ export class SyncController {
|
||||
});
|
||||
} else {
|
||||
// Type not found by type_name - check by display_name (case-insensitive)
|
||||
const db = services.schemaRepo.db;
|
||||
const db = services.schemaRepo.getDatabaseAdapter();
|
||||
const byDisplayName = await db.queryOne<{ enabled: boolean | number; type_name: string | null; display_name: string }>(
|
||||
`SELECT enabled, type_name, display_name FROM object_types WHERE display_name ILIKE ? LIMIT 1`,
|
||||
[`%${typeName}%`]
|
||||
@@ -247,14 +259,14 @@ export class SyncController {
|
||||
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Object type ${typeName} not found. Available enabled types: ${enabledTypes.map(t => t.typeName).join(', ') || 'none'}. Please run schema sync first.`,
|
||||
error: `Object type ${typeName} not found. Available enabled types: ${enabledTypes.map((t: { typeName?: string }) => t.typeName).filter(Boolean).join(', ') || 'none'}. Please run schema sync first.`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await services.objectSyncService.syncObjectType(
|
||||
type.schemaId,
|
||||
type.schemaId.toString(),
|
||||
type.objectTypeId,
|
||||
type.typeName,
|
||||
type.displayName
|
||||
|
||||
@@ -249,7 +249,7 @@ app.listen(PORT, async () => {
|
||||
if (db) {
|
||||
await db.ensureInitialized?.();
|
||||
try {
|
||||
const schemaRow = await db.queryOne<{ count: number }>(
|
||||
const schemaRow = await (db.queryOne as <T>(sql: string, params?: any[]) => Promise<T | null>)<{ count: number }>(
|
||||
`SELECT COUNT(*) as count FROM schemas`
|
||||
);
|
||||
hasSchemas = (schemaRow?.count || 0) > 0;
|
||||
|
||||
@@ -47,6 +47,13 @@ export interface AttributeRecord {
|
||||
|
||||
export class SchemaRepository {
|
||||
constructor(private db: DatabaseAdapter) {}
|
||||
|
||||
/**
|
||||
* Get database adapter (for debug/advanced operations)
|
||||
*/
|
||||
getDatabaseAdapter(): DatabaseAdapter {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert a schema
|
||||
|
||||
@@ -98,7 +98,6 @@ router.post('/discover', requirePermission('manage_settings'), async (req, res)
|
||||
schemaCacheService.invalidate(); // Invalidate cache
|
||||
|
||||
res.json({
|
||||
success: result.success,
|
||||
message: 'Schema synchronization completed',
|
||||
...result,
|
||||
});
|
||||
|
||||
@@ -43,7 +43,6 @@ router.post('/discover', async (req: Request, res: Response) => {
|
||||
if (result.schemasProcessed === 0) {
|
||||
logger.warn('Schema configuration: Sync returned 0 schemas - this might indicate an API issue');
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'No schemas found. Please check: 1) JIRA_SERVICE_ACCOUNT_TOKEN is configured correctly, 2) Jira Assets API is accessible, 3) API endpoint /rest/assets/1.0/objectschema/list is available',
|
||||
...result,
|
||||
});
|
||||
@@ -51,7 +50,6 @@ router.post('/discover', async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: result.success,
|
||||
message: 'Schema synchronization completed successfully',
|
||||
schemasDiscovered: result.schemasProcessed,
|
||||
objectTypesDiscovered: result.objectTypesProcessed,
|
||||
@@ -88,7 +86,11 @@ router.get('/object-types', async (req: Request, res: Response) => {
|
||||
*/
|
||||
router.patch('/object-types/:id/enabled', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
||||
if (!id) {
|
||||
res.status(400).json({ error: 'id parameter required' });
|
||||
return;
|
||||
}
|
||||
const { enabled } = req.body;
|
||||
|
||||
if (typeof enabled !== 'boolean') {
|
||||
@@ -187,7 +189,12 @@ router.patch('/schemas/:schemaId/search-enabled', async (req: Request, res: Resp
|
||||
return;
|
||||
}
|
||||
|
||||
await schemaConfigurationService.setSchemaSearchEnabled(schemaId, searchEnabled);
|
||||
const schemaIdStr = Array.isArray(schemaId) ? schemaId[0] : schemaId;
|
||||
if (!schemaIdStr) {
|
||||
res.status(400).json({ error: 'schemaId parameter required' });
|
||||
return;
|
||||
}
|
||||
await schemaConfigurationService.setSchemaSearchEnabled(schemaIdStr, searchEnabled);
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
|
||||
@@ -132,7 +132,7 @@ export interface SyncProgress {
|
||||
// SchemaSyncService Implementation
|
||||
// =============================================================================
|
||||
|
||||
class SchemaSyncService {
|
||||
export class SchemaSyncService {
|
||||
private db: DatabaseAdapter;
|
||||
private isPostgres: boolean;
|
||||
private baseUrl: string;
|
||||
@@ -342,7 +342,7 @@ class SchemaSyncService {
|
||||
|
||||
// CRITICAL: Jira sometimes returns type=1 (integer) for reference attributes!
|
||||
// The presence of referenceObjectTypeId is the true indicator of a reference type.
|
||||
const refTypeId = attr.referenceObjectTypeId || attr.referenceObject?.id || attr.referenceType?.id;
|
||||
const refTypeId = attr.referenceObjectTypeId || attr.referenceObjectType?.id || attr.referenceType?.id;
|
||||
if (refTypeId) {
|
||||
type = 'reference';
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class ServiceFactory {
|
||||
this.cacheRepo = new ObjectCacheRepository(db);
|
||||
|
||||
// Initialize services
|
||||
this.schemaSyncService = new SchemaSyncService(this.schemaRepo);
|
||||
this.schemaSyncService = new SchemaSyncService();
|
||||
this.objectSyncService = new ObjectSyncService(this.schemaRepo, this.cacheRepo);
|
||||
this.payloadProcessor = new PayloadProcessor(this.schemaRepo, this.cacheRepo);
|
||||
this.queryService = new QueryService(this.schemaRepo, this.cacheRepo);
|
||||
|
||||
@@ -105,8 +105,9 @@ export async function loadBIAData(): Promise<BIARecord[]> {
|
||||
// Read file using readFileSync and then parse with ExcelJS
|
||||
const fileBuffer = readFileSync(biaFilePath);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
// ExcelJS accepts Buffer, but TypeScript types may be strict - use type assertion
|
||||
await workbook.xlsx.load(fileBuffer as Buffer);
|
||||
// ExcelJS accepts Buffer, but TypeScript types may be strict
|
||||
// Use type assertion to satisfy TypeScript's strict Buffer type checking
|
||||
await workbook.xlsx.load(fileBuffer as any);
|
||||
const worksheet = workbook.worksheets[0]; // First sheet
|
||||
|
||||
// Converteer naar 2D array formaat (zoals xlsx.utils.sheet_to_json met header: 1)
|
||||
|
||||
@@ -98,7 +98,7 @@ class CMDBService {
|
||||
|
||||
if (result.objects.length === 0) return null;
|
||||
|
||||
const parsed = jiraAssetsClient.parseObject<T>(result.objects[0]);
|
||||
const parsed = await jiraAssetsClient.parseObject<T>(result.objects[0]);
|
||||
if (parsed) {
|
||||
await cacheStore.upsertObject(typeName, parsed);
|
||||
await cacheStore.extractAndStoreRelations(typeName, parsed);
|
||||
@@ -212,7 +212,7 @@ class CMDBService {
|
||||
});
|
||||
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
const validResults = batchResults.filter((obj): obj is T => obj !== null);
|
||||
const validResults = batchResults.filter((obj): obj is NonNullable<typeof obj> => obj !== null) as T[];
|
||||
results.push(...validResults);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { logger } from './logger.js';
|
||||
import { normalizedCacheStore as cacheStore } from './normalizedCacheStore.js';
|
||||
import { jiraAssetsClient, JiraObjectNotFoundError } from './jiraAssetsClient.js';
|
||||
import type { CMDBObject } from '../generated/jira-types.js';
|
||||
import type { DatabaseAdapter } from './database/interface.js';
|
||||
|
||||
export interface BrokenReference {
|
||||
object_id: string;
|
||||
@@ -150,7 +151,8 @@ class DataIntegrityService {
|
||||
// 1. Check cache first
|
||||
const db = (cacheStore as any).db;
|
||||
if (db) {
|
||||
const objRow = await db.queryOne<{
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const objRow = await typedDb.queryOne<{
|
||||
id: string;
|
||||
object_type_name: string;
|
||||
}>(`
|
||||
|
||||
@@ -12,6 +12,7 @@ import { normalizedCacheStore } from './normalizedCacheStore.js';
|
||||
import { jiraAssetsClient } from './jiraAssetsClient.js';
|
||||
import { jiraAssetsService } from './jiraAssets.js';
|
||||
import { logger } from './logger.js';
|
||||
import type { DatabaseAdapter } from './database/interface.js';
|
||||
import type {
|
||||
ApplicationComponent,
|
||||
IctGovernanceModel,
|
||||
@@ -126,7 +127,8 @@ async function getDescriptionFromDatabase(objectId: string): Promise<string | nu
|
||||
const descriptionFieldNames = ['description', 'Description', 'DESCRIPTION'];
|
||||
|
||||
// First, get the object to find its type
|
||||
const objRow = await db.queryOne<{ object_type_name: string }>(`
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const objRow = await typedDb.queryOne<{ object_type_name: string }>(`
|
||||
SELECT object_type_name FROM objects WHERE id = ?
|
||||
`, [objectId]);
|
||||
|
||||
@@ -134,7 +136,7 @@ async function getDescriptionFromDatabase(objectId: string): Promise<string | nu
|
||||
|
||||
// Try each possible description field name
|
||||
for (const fieldName of descriptionFieldNames) {
|
||||
const descRow = await db.queryOne<{ text_value: string }>(`
|
||||
const descRow = await typedDb.queryOne<{ text_value: string }>(`
|
||||
SELECT av.text_value
|
||||
FROM attribute_values av
|
||||
JOIN attributes a ON av.attribute_id = a.id
|
||||
@@ -173,7 +175,8 @@ async function toReferenceValue(ref: ObjectReference | null | undefined): Promis
|
||||
await db.ensureInitialized?.();
|
||||
|
||||
// Get basic object info from database
|
||||
const objRow = await db.queryOne<{
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const objRow = await typedDb.queryOne<{
|
||||
id: string;
|
||||
object_key: string;
|
||||
label: string;
|
||||
@@ -1280,8 +1283,342 @@ export const dataService = {
|
||||
},
|
||||
|
||||
async getTeamDashboardData(excludedStatuses: ApplicationStatus[] = []): Promise<TeamDashboardData> {
|
||||
// Always get from Jira Assets API (has proper Team/Subteam field parsing)
|
||||
return jiraAssetsService.getTeamDashboardData(excludedStatuses);
|
||||
// Load from database cache instead of API for better performance
|
||||
logger.info(`Loading team dashboard data from database cache (excluding: ${excludedStatuses.join(', ')})`);
|
||||
|
||||
try {
|
||||
// Get all ApplicationComponents from database cache
|
||||
const allApplications = await cmdbService.getObjects<ApplicationComponent>('ApplicationComponent');
|
||||
logger.info(`Loaded ${allApplications.length} applications from database cache`);
|
||||
|
||||
// Convert to ApplicationListItem
|
||||
const applicationListItems = await Promise.all(
|
||||
allApplications.map(app => toApplicationListItem(app))
|
||||
);
|
||||
|
||||
// Filter out excluded statuses
|
||||
const filteredApplications = excludedStatuses.length > 0
|
||||
? applicationListItems.filter(app => !app.status || !excludedStatuses.includes(app.status))
|
||||
: applicationListItems;
|
||||
|
||||
logger.info(`After status filter: ${filteredApplications.length} applications (excluded: ${excludedStatuses.join(', ')})`);
|
||||
|
||||
// Separate into Platforms, Workloads, and regular applications
|
||||
const platforms: ApplicationListItem[] = [];
|
||||
const workloads: ApplicationListItem[] = [];
|
||||
const regularApplications: ApplicationListItem[] = [];
|
||||
|
||||
for (const app of filteredApplications) {
|
||||
const isPlatform = app.applicationType?.name === 'Platform';
|
||||
const isWorkload = app.platform !== null;
|
||||
|
||||
if (isPlatform) {
|
||||
platforms.push(app);
|
||||
} else if (isWorkload) {
|
||||
workloads.push(app);
|
||||
} else {
|
||||
regularApplications.push(app);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Identified ${platforms.length} platforms, ${workloads.length} workloads, ${regularApplications.length} regular applications`);
|
||||
|
||||
// Group workloads by their platform
|
||||
const workloadsByPlatform = new Map<string, ApplicationListItem[]>();
|
||||
for (const workload of workloads) {
|
||||
const platformId = workload.platform!.objectId;
|
||||
if (!workloadsByPlatform.has(platformId)) {
|
||||
workloadsByPlatform.set(platformId, []);
|
||||
}
|
||||
workloadsByPlatform.get(platformId)!.push(workload);
|
||||
}
|
||||
|
||||
// Helper functions for FTE calculations
|
||||
const getEffectiveFTE = (app: ApplicationListItem): number => {
|
||||
return app.overrideFTE !== null && app.overrideFTE !== undefined
|
||||
? app.overrideFTE
|
||||
: (app.requiredEffortApplicationManagement || 0);
|
||||
};
|
||||
|
||||
const getMinFTE = (app: ApplicationListItem): number => {
|
||||
if (app.overrideFTE !== null && app.overrideFTE !== undefined) {
|
||||
return app.overrideFTE;
|
||||
}
|
||||
return app.minFTE ?? app.requiredEffortApplicationManagement ?? 0;
|
||||
};
|
||||
|
||||
const getMaxFTE = (app: ApplicationListItem): number => {
|
||||
if (app.overrideFTE !== null && app.overrideFTE !== undefined) {
|
||||
return app.overrideFTE;
|
||||
}
|
||||
return app.maxFTE ?? app.requiredEffortApplicationManagement ?? 0;
|
||||
};
|
||||
|
||||
// Build PlatformWithWorkloads structures
|
||||
const platformsWithWorkloads: PlatformWithWorkloads[] = [];
|
||||
for (const platform of platforms) {
|
||||
const platformWorkloads = workloadsByPlatform.get(platform.id) || [];
|
||||
const platformEffort = getEffectiveFTE(platform);
|
||||
const workloadsEffort = platformWorkloads.reduce((sum, w) => sum + getEffectiveFTE(w), 0);
|
||||
|
||||
platformsWithWorkloads.push({
|
||||
platform,
|
||||
workloads: platformWorkloads,
|
||||
platformEffort,
|
||||
workloadsEffort,
|
||||
totalEffort: platformEffort + workloadsEffort,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to calculate subteam KPIs
|
||||
const calculateSubteamKPIs = (
|
||||
regularApps: ApplicationListItem[],
|
||||
platformsList: PlatformWithWorkloads[]
|
||||
) => {
|
||||
const regularEffort = regularApps.reduce((sum, app) => sum + getEffectiveFTE(app), 0);
|
||||
const platformsEffort = platformsList.reduce((sum, p) => sum + p.totalEffort, 0);
|
||||
const totalEffort = regularEffort + platformsEffort;
|
||||
|
||||
const regularMinEffort = regularApps.reduce((sum, app) => sum + getMinFTE(app), 0);
|
||||
const regularMaxEffort = regularApps.reduce((sum, app) => sum + getMaxFTE(app), 0);
|
||||
const platformsMinEffort = platformsList.reduce((sum, p) => {
|
||||
const platformMin = getMinFTE(p.platform);
|
||||
const workloadsMin = p.workloads.reduce((s, w) => s + getMinFTE(w), 0);
|
||||
return sum + platformMin + workloadsMin;
|
||||
}, 0);
|
||||
const platformsMaxEffort = platformsList.reduce((sum, p) => {
|
||||
const platformMax = getMaxFTE(p.platform);
|
||||
const workloadsMax = p.workloads.reduce((s, w) => s + getMaxFTE(w), 0);
|
||||
return sum + platformMax + workloadsMax;
|
||||
}, 0);
|
||||
const minEffort = regularMinEffort + platformsMinEffort;
|
||||
const maxEffort = regularMaxEffort + platformsMaxEffort;
|
||||
|
||||
const platformsCount = platformsList.length;
|
||||
const workloadsCount = platformsList.reduce((sum, p) => sum + p.workloads.length, 0);
|
||||
const applicationCount = regularApps.length + platformsCount + workloadsCount;
|
||||
|
||||
const byGovernanceModel: Record<string, number> = {};
|
||||
for (const app of regularApps) {
|
||||
const govModel = app.governanceModel?.name || 'Niet ingesteld';
|
||||
byGovernanceModel[govModel] = (byGovernanceModel[govModel] || 0) + 1;
|
||||
}
|
||||
for (const pwl of platformsList) {
|
||||
const govModel = pwl.platform.governanceModel?.name || 'Niet ingesteld';
|
||||
byGovernanceModel[govModel] = (byGovernanceModel[govModel] || 0) + 1;
|
||||
for (const workload of pwl.workloads) {
|
||||
const workloadGovModel = workload.governanceModel?.name || 'Niet ingesteld';
|
||||
byGovernanceModel[workloadGovModel] = (byGovernanceModel[workloadGovModel] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { totalEffort, minEffort, maxEffort, applicationCount, byGovernanceModel };
|
||||
};
|
||||
|
||||
// HIERARCHICAL GROUPING: Team -> Subteam -> Applications
|
||||
type SubteamData = {
|
||||
subteam: ReferenceValue | null;
|
||||
regular: ApplicationListItem[];
|
||||
platforms: PlatformWithWorkloads[];
|
||||
};
|
||||
|
||||
type TeamData = {
|
||||
team: ReferenceValue | null;
|
||||
subteams: Map<string, SubteamData>;
|
||||
};
|
||||
|
||||
// Load Subteam -> Team mapping (still from API but cached, so should be fast)
|
||||
const subteamToTeamMap = await jiraAssetsService.getSubteamToTeamMapping();
|
||||
logger.info(`Loaded ${subteamToTeamMap.size} subteam->team mappings`);
|
||||
|
||||
const teamMap = new Map<string, TeamData>();
|
||||
const unassignedData: SubteamData = {
|
||||
subteam: null,
|
||||
regular: [],
|
||||
platforms: [],
|
||||
};
|
||||
|
||||
// Helper to get Team from Subteam
|
||||
const getTeamForSubteam = (subteam: ReferenceValue | null): ReferenceValue | null => {
|
||||
if (!subteam) return null;
|
||||
return subteamToTeamMap.get(subteam.objectId) || null;
|
||||
};
|
||||
|
||||
// Group regular applications by Team -> Subteam
|
||||
for (const app of regularApplications) {
|
||||
const subteam = app.applicationSubteam;
|
||||
const team = getTeamForSubteam(subteam);
|
||||
|
||||
if (team) {
|
||||
const teamId = team.objectId;
|
||||
if (!teamMap.has(teamId)) {
|
||||
teamMap.set(teamId, { team, subteams: new Map() });
|
||||
}
|
||||
const teamData = teamMap.get(teamId)!;
|
||||
|
||||
const subteamId = subteam?.objectId || 'no-subteam';
|
||||
if (!teamData.subteams.has(subteamId)) {
|
||||
teamData.subteams.set(subteamId, {
|
||||
subteam: subteam || null,
|
||||
regular: [],
|
||||
platforms: [],
|
||||
});
|
||||
}
|
||||
teamData.subteams.get(subteamId)!.regular.push(app);
|
||||
} else if (subteam) {
|
||||
// Has subteam but no team - put under a virtual "Geen Team" team
|
||||
const noTeamId = 'no-team';
|
||||
if (!teamMap.has(noTeamId)) {
|
||||
teamMap.set(noTeamId, {
|
||||
team: { objectId: noTeamId, key: 'NO-TEAM', name: 'Geen Team' },
|
||||
subteams: new Map()
|
||||
});
|
||||
}
|
||||
const teamData = teamMap.get(noTeamId)!;
|
||||
const subteamId = subteam.objectId;
|
||||
if (!teamData.subteams.has(subteamId)) {
|
||||
teamData.subteams.set(subteamId, {
|
||||
subteam: subteam,
|
||||
regular: [],
|
||||
platforms: [],
|
||||
});
|
||||
}
|
||||
teamData.subteams.get(subteamId)!.regular.push(app);
|
||||
} else {
|
||||
// No subteam assigned - goes to unassigned
|
||||
unassignedData.regular.push(app);
|
||||
}
|
||||
}
|
||||
|
||||
// Group platforms by Team -> Subteam
|
||||
for (const pwl of platformsWithWorkloads) {
|
||||
const platform = pwl.platform;
|
||||
const subteam = platform.applicationSubteam;
|
||||
const team = getTeamForSubteam(subteam);
|
||||
|
||||
if (team) {
|
||||
const teamId = team.objectId;
|
||||
if (!teamMap.has(teamId)) {
|
||||
teamMap.set(teamId, { team, subteams: new Map() });
|
||||
}
|
||||
const teamData = teamMap.get(teamId)!;
|
||||
|
||||
const subteamId = subteam?.objectId || 'no-subteam';
|
||||
if (!teamData.subteams.has(subteamId)) {
|
||||
teamData.subteams.set(subteamId, {
|
||||
subteam: subteam || null,
|
||||
regular: [],
|
||||
platforms: [],
|
||||
});
|
||||
}
|
||||
teamData.subteams.get(subteamId)!.platforms.push(pwl);
|
||||
} else if (subteam) {
|
||||
// Has subteam but no team - put under "Geen Team"
|
||||
const noTeamId = 'no-team';
|
||||
if (!teamMap.has(noTeamId)) {
|
||||
teamMap.set(noTeamId, {
|
||||
team: { objectId: noTeamId, key: 'NO-TEAM', name: 'Geen Team' },
|
||||
subteams: new Map()
|
||||
});
|
||||
}
|
||||
const teamData = teamMap.get(noTeamId)!;
|
||||
const subteamId = subteam.objectId;
|
||||
if (!teamData.subteams.has(subteamId)) {
|
||||
teamData.subteams.set(subteamId, {
|
||||
subteam: subteam,
|
||||
regular: [],
|
||||
platforms: [],
|
||||
});
|
||||
}
|
||||
teamData.subteams.get(subteamId)!.platforms.push(pwl);
|
||||
} else {
|
||||
// No subteam assigned - goes to unassigned
|
||||
unassignedData.platforms.push(pwl);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Team dashboard: grouped ${regularApplications.length} regular apps and ${platformsWithWorkloads.length} platforms into ${teamMap.size} teams`);
|
||||
|
||||
// Build the hierarchical result structure
|
||||
const teams: TeamDashboardTeam[] = [];
|
||||
|
||||
// Process teams in alphabetical order
|
||||
const sortedTeamIds = Array.from(teamMap.keys()).sort((a, b) => {
|
||||
const teamA = teamMap.get(a)!.team?.name || '';
|
||||
const teamB = teamMap.get(b)!.team?.name || '';
|
||||
return teamA.localeCompare(teamB, 'nl', { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
for (const teamId of sortedTeamIds) {
|
||||
const teamData = teamMap.get(teamId)!;
|
||||
const fullTeam = teamData.team;
|
||||
|
||||
const subteams: TeamDashboardSubteam[] = [];
|
||||
|
||||
// Sort subteams alphabetically (with "no-subteam" at the end)
|
||||
const sortedSubteamEntries = Array.from(teamData.subteams.entries()).sort((a, b) => {
|
||||
if (a[0] === 'no-subteam') return 1;
|
||||
if (b[0] === 'no-subteam') return -1;
|
||||
const nameA = a[1].subteam?.name || '';
|
||||
const nameB = b[1].subteam?.name || '';
|
||||
return nameA.localeCompare(nameB, 'nl', { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
for (const [subteamId, subteamData] of sortedSubteamEntries) {
|
||||
const kpis = calculateSubteamKPIs(subteamData.regular, subteamData.platforms);
|
||||
|
||||
subteams.push({
|
||||
subteam: subteamData.subteam,
|
||||
applications: subteamData.regular,
|
||||
platforms: subteamData.platforms,
|
||||
...kpis,
|
||||
});
|
||||
}
|
||||
|
||||
// Aggregate team KPIs from all subteams
|
||||
const teamTotalEffort = subteams.reduce((sum, s) => sum + s.totalEffort, 0);
|
||||
const teamMinEffort = subteams.reduce((sum, s) => sum + s.minEffort, 0);
|
||||
const teamMaxEffort = subteams.reduce((sum, s) => sum + s.maxEffort, 0);
|
||||
const teamApplicationCount = subteams.reduce((sum, s) => sum + s.applicationCount, 0);
|
||||
const teamByGovernanceModel: Record<string, number> = {};
|
||||
for (const subteam of subteams) {
|
||||
for (const [model, count] of Object.entries(subteam.byGovernanceModel)) {
|
||||
teamByGovernanceModel[model] = (teamByGovernanceModel[model] || 0) + count;
|
||||
}
|
||||
}
|
||||
|
||||
teams.push({
|
||||
team: fullTeam,
|
||||
subteams,
|
||||
totalEffort: teamTotalEffort,
|
||||
minEffort: teamMinEffort,
|
||||
maxEffort: teamMaxEffort,
|
||||
applicationCount: teamApplicationCount,
|
||||
byGovernanceModel: teamByGovernanceModel,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate unassigned KPIs
|
||||
const unassignedKPIs = calculateSubteamKPIs(unassignedData.regular, unassignedData.platforms);
|
||||
|
||||
const result: TeamDashboardData = {
|
||||
teams,
|
||||
unassigned: {
|
||||
subteam: null,
|
||||
applications: unassignedData.regular,
|
||||
platforms: unassignedData.platforms,
|
||||
...unassignedKPIs,
|
||||
},
|
||||
};
|
||||
|
||||
logger.info(`Team dashboard data loaded from database: ${teams.length} teams, ${unassignedData.regular.length + unassignedData.platforms.length} unassigned apps`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get team dashboard data from database', error);
|
||||
// Fallback to API if database fails
|
||||
logger.warn('Falling back to API for team dashboard data');
|
||||
return jiraAssetsService.getTeamDashboardData(excludedStatuses);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
|
||||
import { logger } from '../logger.js';
|
||||
import { normalizedCacheStore } from '../normalizedCacheStore.js';
|
||||
import type { DatabaseAdapter } from './interface.js';
|
||||
|
||||
export async function fixObjectTypesConstraints(): Promise<void> {
|
||||
const db = (normalizedCacheStore as any).db;
|
||||
const db = (normalizedCacheStore as any).db as DatabaseAdapter;
|
||||
if (!db) {
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
await db.ensureInitialized?.();
|
||||
await (db as any).ensureInitialized?.();
|
||||
|
||||
logger.info('Migration: Fixing UNIQUE constraints on object_types table...');
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { logger } from '../logger.js';
|
||||
import { normalizedCacheStore } from '../normalizedCacheStore.js';
|
||||
import type { DatabaseAdapter } from './interface.js';
|
||||
|
||||
export async function migrateToNormalizedSchema(): Promise<void> {
|
||||
const db = (normalizedCacheStore as any).db;
|
||||
@@ -23,7 +24,7 @@ export async function migrateToNormalizedSchema(): Promise<void> {
|
||||
logger.info('Migration: Starting migration to normalized schema structure...');
|
||||
|
||||
try {
|
||||
await db.transaction(async (txDb) => {
|
||||
await db.transaction(async (txDb: DatabaseAdapter) => {
|
||||
// Step 1: Check if configured_object_types table exists
|
||||
let configuredTableExists = false;
|
||||
try {
|
||||
@@ -172,14 +173,14 @@ export async function migrateToNormalizedSchema(): Promise<void> {
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'object_types'
|
||||
`);
|
||||
hasSchemaId = columns.some(c => c.column_name === 'schema_id');
|
||||
hasEnabled = columns.some(c => c.column_name === 'enabled');
|
||||
hasSchemaId = columns.some((c: { column_name: string }) => c.column_name === 'schema_id');
|
||||
hasEnabled = columns.some((c: { column_name: string }) => c.column_name === 'enabled');
|
||||
} else {
|
||||
const tableInfo = await txDb.query<{ name: string }>(`
|
||||
PRAGMA table_info(object_types)
|
||||
`);
|
||||
hasSchemaId = tableInfo.some(c => c.name === 'schema_id');
|
||||
hasEnabled = tableInfo.some(c => c.name === 'enabled');
|
||||
hasSchemaId = tableInfo.some((c: { name: string }) => c.name === 'schema_id');
|
||||
hasEnabled = tableInfo.some((c: { name: string }) => c.name === 'enabled');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Migration: Could not check object_types columns', error);
|
||||
|
||||
@@ -15,6 +15,8 @@ import type {
|
||||
ApplicationUpdateRequest,
|
||||
TeamDashboardData,
|
||||
} from '../types/index.js';
|
||||
import type { CMDBObjectTypeName } from '../generated/jira-types.js';
|
||||
import type { DatabaseAdapter } from './database/interface.js';
|
||||
|
||||
// Attribute name mappings (these should match your Jira Assets schema)
|
||||
const ATTRIBUTE_NAMES = {
|
||||
@@ -125,7 +127,8 @@ class JiraAssetsService {
|
||||
if (!db) return null;
|
||||
|
||||
await db.ensureInitialized?.();
|
||||
const schemaRow = await db.queryOne<{ jira_schema_id: string }>(
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const schemaRow = await typedDb.queryOne<{ jira_schema_id: string }>(
|
||||
`SELECT jira_schema_id FROM schemas ORDER BY jira_schema_id LIMIT 1`
|
||||
);
|
||||
return schemaRow?.jira_schema_id || null;
|
||||
@@ -145,11 +148,12 @@ class JiraAssetsService {
|
||||
if (!db) return [];
|
||||
|
||||
await db.ensureInitialized?.();
|
||||
const schemaRows = await db.query<{ jira_schema_id: string }>(
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const schemaRows = await typedDb.query<{ jira_schema_id: string }>(
|
||||
`SELECT DISTINCT jira_schema_id FROM schemas WHERE search_enabled = ? ORDER BY jira_schema_id`,
|
||||
[db.isPostgres ? true : 1]
|
||||
[typedDb.isPostgres ? true : 1]
|
||||
);
|
||||
return schemaRows.map(row => row.jira_schema_id);
|
||||
return schemaRows.map((row: { jira_schema_id: string }) => row.jira_schema_id);
|
||||
} catch (error) {
|
||||
logger.warn('JiraAssets: Failed to get all schema IDs', error);
|
||||
return [];
|
||||
|
||||
@@ -14,6 +14,7 @@ import { schemaDiscoveryService } from './schemaDiscoveryService.js';
|
||||
import { queryBuilder } from './queryBuilder.js';
|
||||
import { NORMALIZED_SCHEMA_POSTGRES, NORMALIZED_SCHEMA_SQLITE } from './database/normalized-schema.js';
|
||||
import type { AttributeDefinition } from '../generated/jira-schema.js';
|
||||
import { jiraAssetsClient } from './jiraAssetsClient.js';
|
||||
|
||||
// Re-export interfaces for compatibility
|
||||
export interface CacheStats {
|
||||
@@ -246,7 +247,7 @@ class NormalizedCacheStore {
|
||||
rows.map(row => this.reconstructObject<T>(row.id, typeName))
|
||||
);
|
||||
|
||||
const validObjects = objects.filter((obj): obj is T => obj !== null);
|
||||
const validObjects = objects.filter((obj): obj is NonNullable<typeof obj> => obj !== null) as T[];
|
||||
logger.debug(`NormalizedCacheStore: Successfully reconstructed ${validObjects.length}/${rows.length} objects of type ${typeName}`);
|
||||
|
||||
return validObjects;
|
||||
@@ -294,7 +295,7 @@ class NormalizedCacheStore {
|
||||
rows.map(row => this.reconstructObject<T>(row.id, typeName))
|
||||
);
|
||||
|
||||
return objects.filter((obj): obj is T => obj !== null);
|
||||
return objects.filter((obj): obj is NonNullable<typeof obj> => obj !== null) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1129,8 +1130,9 @@ class NormalizedCacheStore {
|
||||
`, [id]);
|
||||
|
||||
if (row) {
|
||||
this.referenceCache.set(id, row);
|
||||
return row;
|
||||
const cached = { id: row.id, objectKey: row.object_key, label: row.label };
|
||||
this.referenceCache.set(id, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1190,7 +1192,7 @@ class NormalizedCacheStore {
|
||||
);
|
||||
|
||||
return {
|
||||
objects: objects.filter((obj): obj is T => obj !== null),
|
||||
objects: objects.filter((obj): obj is NonNullable<typeof obj> => obj !== null) as T[],
|
||||
total
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -1363,7 +1365,7 @@ class NormalizedCacheStore {
|
||||
rows.map(row => this.reconstructObject<T>(row.id, targetTypeName))
|
||||
);
|
||||
|
||||
return objects.filter((obj): obj is T => obj !== null);
|
||||
return objects.filter((obj): obj is NonNullable<typeof obj> => obj !== null) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1424,7 +1426,7 @@ class NormalizedCacheStore {
|
||||
rows.map(row => this.reconstructObject<T>(row.id, sourceTypeName))
|
||||
);
|
||||
|
||||
const validObjects = objects.filter((obj): obj is T => obj !== null);
|
||||
const validObjects = objects.filter((obj): obj is NonNullable<typeof obj> => obj !== null) as T[];
|
||||
logger.debug(`NormalizedCacheStore: Successfully reconstructed ${validObjects.length} objects`);
|
||||
|
||||
return validObjects;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { logger } from './logger.js';
|
||||
import { normalizedCacheStore } from './normalizedCacheStore.js';
|
||||
import { config } from '../config/env.js';
|
||||
import type { CMDBObjectTypeName } from '../generated/jira-types.js';
|
||||
import type { DatabaseAdapter } from './database/interface.js';
|
||||
|
||||
export interface SchemaMapping {
|
||||
objectTypeName: string;
|
||||
@@ -61,7 +62,8 @@ class SchemaMappingService {
|
||||
|
||||
await db.ensureInitialized?.();
|
||||
|
||||
const rows = await db.query<{
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const rows = await typedDb.query<{
|
||||
object_type_name: string;
|
||||
schema_id: string;
|
||||
enabled: boolean | number;
|
||||
@@ -73,7 +75,7 @@ class SchemaMappingService {
|
||||
ORDER BY object_type_name
|
||||
`);
|
||||
|
||||
return rows.map(row => ({
|
||||
return rows.map((row: { object_type_name: string; schema_id: string; enabled: boolean | number; created_at: string; updated_at: string }) => ({
|
||||
objectTypeName: row.object_type_name,
|
||||
schemaId: row.schema_id,
|
||||
enabled: typeof row.enabled === 'boolean' ? row.enabled : row.enabled === 1,
|
||||
@@ -158,7 +160,8 @@ class SchemaMappingService {
|
||||
const db = (normalizedCacheStore as any).db;
|
||||
if (db) {
|
||||
await db.ensureInitialized?.();
|
||||
const row = await db.queryOne<{ enabled: boolean | number }>(`
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const row = await typedDb.queryOne<{ enabled: boolean | number }>(`
|
||||
SELECT enabled FROM schema_mappings WHERE object_type_name = ?
|
||||
`, [objectTypeName]);
|
||||
|
||||
@@ -195,7 +198,8 @@ class SchemaMappingService {
|
||||
await db.ensureInitialized?.();
|
||||
|
||||
// Use parameterized query to avoid boolean/integer comparison issues
|
||||
const rows = await db.query<{
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const rows = await typedDb.query<{
|
||||
object_type_name: string;
|
||||
schema_id: string;
|
||||
enabled: boolean | number;
|
||||
@@ -243,7 +247,8 @@ class SchemaMappingService {
|
||||
|
||||
try {
|
||||
// Get all object types with their mappings
|
||||
const rows = await db.query<{
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const rows = await typedDb.query<{
|
||||
type_name: string;
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
@@ -274,7 +279,8 @@ class SchemaMappingService {
|
||||
const db = (normalizedCacheStore as any).db;
|
||||
if (db) {
|
||||
await db.ensureInitialized?.();
|
||||
const schemaRow = await db.queryOne<{ jira_schema_id: string }>(
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const schemaRow = await typedDb.queryOne<{ jira_schema_id: string }>(
|
||||
`SELECT jira_schema_id FROM schemas ORDER BY jira_schema_id LIMIT 1`
|
||||
);
|
||||
defaultSchemaId = schemaRow?.jira_schema_id || null;
|
||||
@@ -283,7 +289,7 @@ class SchemaMappingService {
|
||||
logger.warn('SchemaMapping: Failed to get default schema ID from database', error);
|
||||
}
|
||||
|
||||
return rows.map(row => ({
|
||||
return rows.map((row: { type_name: string; display_name: string; description: string | null; object_count: number; sync_priority: number; schema_id: string | null; enabled: number | boolean | null }) => ({
|
||||
typeName: row.type_name,
|
||||
displayName: row.display_name,
|
||||
description: row.description,
|
||||
@@ -291,8 +297,8 @@ class SchemaMappingService {
|
||||
enabled: row.enabled === null
|
||||
? true // Default: enabled if no mapping exists
|
||||
: (typeof row.enabled === 'boolean' ? row.enabled : row.enabled === 1),
|
||||
objectCount: row.object_count,
|
||||
syncPriority: row.sync_priority,
|
||||
objectCount: row.object_count || 0,
|
||||
syncPriority: row.sync_priority || 0,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('SchemaMappingService: Failed to get object types with config', error);
|
||||
@@ -312,7 +318,8 @@ class SchemaMappingService {
|
||||
await db.ensureInitialized?.();
|
||||
|
||||
// Check if mapping exists
|
||||
const existing = await db.queryOne<{ schema_id: string }>(`
|
||||
const typedDb = db as DatabaseAdapter;
|
||||
const existing = await typedDb.queryOne<{ schema_id: string }>(`
|
||||
SELECT schema_id FROM schema_mappings WHERE object_type_name = ?
|
||||
`, [objectTypeName]);
|
||||
|
||||
|
||||
165
docs/AZURE-ACR-NAMING-RECOMMENDATION.md
Normal file
165
docs/AZURE-ACR-NAMING-RECOMMENDATION.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Azure Container Registry Naming Recommendation
|
||||
|
||||
Recommendations for naming your Azure Container Registry for Zuyderland Application Services.
|
||||
|
||||
## 🎯 Requirements
|
||||
|
||||
Azure Container Registry names must:
|
||||
- Be **globally unique** (across all Azure subscriptions)
|
||||
- Be **5-50 characters** long
|
||||
- Contain **only lowercase alphanumeric characters** (no hyphens, underscores, or special characters)
|
||||
- Be **descriptive** but not too long
|
||||
|
||||
## 💡 Recommended Options
|
||||
|
||||
### Option 1: `zuyderlandacr` ⭐ **RECOMMENDED**
|
||||
|
||||
**Pros:**
|
||||
- ✅ Clear company identification
|
||||
- ✅ Short and memorable (15 characters)
|
||||
- ✅ Generic enough for all Application Services apps
|
||||
- ✅ Easy to type and remember
|
||||
- ✅ Professional appearance
|
||||
|
||||
**Cons:**
|
||||
- ⚠️ Might be taken if Zuyderland already has an ACR
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
ACR_NAME="zuyderlandacr"
|
||||
# Images: zuyderlandacr.azurecr.io/cmdb-insight/backend:latest
|
||||
```
|
||||
|
||||
### Option 2: `zuyderlandsharedacr`
|
||||
|
||||
**Pros:**
|
||||
- ✅ Clearly indicates it's a shared registry
|
||||
- ✅ Company identification
|
||||
- ✅ Good for documentation ("shared ACR")
|
||||
|
||||
**Cons:**
|
||||
- ⚠️ Longer (20 characters)
|
||||
- ⚠️ "shared" might be redundant (ACRs are typically shared)
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
ACR_NAME="zuyderlandsharedacr"
|
||||
# Images: zuyderlandsharedacr.azurecr.io/cmdb-insight/backend:latest
|
||||
```
|
||||
|
||||
### Option 3: `zyldacr` (Abbreviated)
|
||||
|
||||
**Pros:**
|
||||
- ✅ Very short (7 characters)
|
||||
- ✅ Easy to type
|
||||
- ✅ Less likely to be taken
|
||||
|
||||
**Cons:**
|
||||
- ⚠️ Less clear what "zyld" means
|
||||
- ⚠️ Might not be obvious it's Zuyderland
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
ACR_NAME="zyldacr"
|
||||
# Images: zyldacr.azurecr.io/cmdb-insight/backend:latest
|
||||
```
|
||||
|
||||
### Option 4: `zuyderlandappsvcsacr`
|
||||
|
||||
**Pros:**
|
||||
- ✅ Includes department name (Application Services)
|
||||
- ✅ Very specific
|
||||
|
||||
**Cons:**
|
||||
- ⚠️ Long (23 characters)
|
||||
- ⚠️ Less flexible if other departments want to use it
|
||||
- ⚠️ Harder to type
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
ACR_NAME="zuyderlandappsvcsacr"
|
||||
# Images: zuyderlandappsvcsacr.azurecr.io/cmdb-insight/backend:latest
|
||||
```
|
||||
|
||||
## 🏆 Final Recommendation
|
||||
|
||||
**Use: `zuyderlandacr`**
|
||||
|
||||
**Reasoning:**
|
||||
1. **Clear and professional**: Immediately identifies as Zuyderland
|
||||
2. **Appropriate length**: Not too short (unclear) or too long (hard to type)
|
||||
3. **Generic enough**: Can be used by all Application Services applications
|
||||
4. **Future-proof**: Works for any department or team within Zuyderland
|
||||
5. **Standard pattern**: Follows common naming convention (`companynameacr`)
|
||||
|
||||
## 🔍 Check Availability
|
||||
|
||||
Before creating, check if the name is available:
|
||||
|
||||
```bash
|
||||
# Try to check if name exists (will fail if available, which is good)
|
||||
az acr show --name zuyderlandacr --resource-group dummy-rg 2>&1 | grep -q "could not be found" && echo "Name available!" || echo "Name might be taken"
|
||||
|
||||
# Or try to create (will fail if taken)
|
||||
az acr check-name --name zuyderlandacr
|
||||
```
|
||||
|
||||
## 📝 Alternative if Name is Taken
|
||||
|
||||
If `zuyderlandacr` is already taken, try:
|
||||
|
||||
1. `zuyderlandacr01` - Add number suffix
|
||||
2. `zuyderlandacrprod` - Add environment suffix
|
||||
3. `zyldacr` - Use abbreviation
|
||||
4. `zuyderlandregistry` - Use full word "registry"
|
||||
5. `zuyderlandcontainers` - Use "containers" instead of "acr"
|
||||
|
||||
## 🎯 Naming Pattern
|
||||
|
||||
For consistency across Zuyderland, consider this pattern:
|
||||
|
||||
```
|
||||
zuyderlandacr ← Shared ACR for all apps (recommended)
|
||||
zuyderlandacrdev ← Development ACR (if needed)
|
||||
zuyderlandacrprod ← Production ACR (if separate)
|
||||
```
|
||||
|
||||
**But for most cases, one shared ACR (`zuyderlandacr`) is sufficient.**
|
||||
|
||||
## 📋 Update Your Configuration
|
||||
|
||||
Once you've chosen a name, update:
|
||||
|
||||
### 1. Setup Script
|
||||
```bash
|
||||
# In scripts/setup-azure-resources.sh
|
||||
ACR_NAME="zuyderlandacr"
|
||||
ACR_RESOURCE_GROUP="rg-shared-services" # Or rg-zuyderland-shared
|
||||
```
|
||||
|
||||
### 2. Pipeline Variables
|
||||
```yaml
|
||||
# In azure-pipelines.yml
|
||||
variables:
|
||||
acrName: 'zuyderlandacr'
|
||||
repositoryName: 'cmdb-insight'
|
||||
```
|
||||
|
||||
### 3. Build Scripts
|
||||
```bash
|
||||
# In scripts/build-and-push-azure.sh
|
||||
export ACR_NAME="zuyderlandacr"
|
||||
```
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] Choose ACR name: `zuyderlandacr` (recommended)
|
||||
- [ ] Check name availability
|
||||
- [ ] Create ACR with chosen name
|
||||
- [ ] Update all configuration files
|
||||
- [ ] Document name for team
|
||||
- [ ] Share ACR name with other Application Services teams
|
||||
|
||||
---
|
||||
|
||||
**Recommended: `zuyderlandacr`** - Clear, professional, and reusable for all Zuyderland Application Services applications.
|
||||
317
docs/AZURE-ACR-SHARED-SETUP.md
Normal file
317
docs/AZURE-ACR-SHARED-SETUP.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Shared Azure Container Registry Setup
|
||||
|
||||
Guide for using a shared Azure Container Registry across multiple applications.
|
||||
|
||||
## 🎯 Why Share ACR?
|
||||
|
||||
**Benefits:**
|
||||
- ✅ **Cost Savings**: One ACR for all applications (€5-20/month vs multiple ACRs)
|
||||
- ✅ **Centralized Management**: All images in one place
|
||||
- ✅ **Easier Collaboration**: Teams can share images
|
||||
- ✅ **Better Resource Utilization**: More efficient use of storage
|
||||
|
||||
**How it works:**
|
||||
- ACR is shared, but each application uses a **unique repository name**
|
||||
- Repository name (`cmdb-insight`) separates your app from others
|
||||
- Images are organized by application: `acr.azurecr.io/app-name/service:tag`
|
||||
|
||||
## 📦 ACR Structure
|
||||
|
||||
```
|
||||
zuyderlandacr.azurecr.io/
|
||||
├── cmdb-insight/ ← This application
|
||||
│ ├── backend:latest
|
||||
│ ├── backend:v1.0.0
|
||||
│ ├── frontend:latest
|
||||
│ └── frontend:v1.0.0
|
||||
├── other-app/ ← Another application
|
||||
│ ├── api:latest
|
||||
│ └── web:latest
|
||||
└── shared-services/ ← Shared base images
|
||||
├── nginx:latest
|
||||
└── node:20-alpine
|
||||
```
|
||||
|
||||
## 🔧 Setup Options
|
||||
|
||||
### Option 1: Use Existing ACR (Recommended)
|
||||
|
||||
If you already have an ACR for other applications:
|
||||
|
||||
```bash
|
||||
# Set your existing ACR details
|
||||
ACR_NAME="your-existing-acr"
|
||||
ACR_RESOURCE_GROUP="rg-shared-services" # Or wherever your ACR is
|
||||
|
||||
# Verify it exists
|
||||
az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP
|
||||
|
||||
# Get login server
|
||||
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP --query loginServer --output tsv)
|
||||
echo "ACR Login Server: $ACR_LOGIN_SERVER"
|
||||
```
|
||||
|
||||
**That's it!** Your images will be stored as:
|
||||
- `your-existing-acr.azurecr.io/cmdb-insight/backend:latest`
|
||||
- `your-existing-acr.azurecr.io/cmdb-insight/frontend:latest`
|
||||
|
||||
### Option 2: Create New Shared ACR
|
||||
|
||||
If you don't have an ACR yet, create one that can be shared:
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
ACR_NAME="zuyderlandacr" # Recommended: company name + "acr"
|
||||
ACR_RESOURCE_GROUP="rg-shared-services" # Shared resource group
|
||||
LOCATION="westeurope"
|
||||
SKU="Standard" # Recommended for shared ACR
|
||||
|
||||
# Create resource group for shared services
|
||||
az group create --name $ACR_RESOURCE_GROUP --location $LOCATION
|
||||
|
||||
# Create ACR
|
||||
az acr create \
|
||||
--resource-group $ACR_RESOURCE_GROUP \
|
||||
--name $ACR_NAME \
|
||||
--sku $SKU \
|
||||
--admin-enabled true
|
||||
|
||||
# Verify
|
||||
az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP
|
||||
```
|
||||
|
||||
## 🚀 Using Shared ACR
|
||||
|
||||
### Build and Push Images
|
||||
|
||||
```bash
|
||||
# Set ACR name
|
||||
export ACR_NAME="zuyderlandacr"
|
||||
export REPO_NAME="cmdb-insight" # This is your app identifier
|
||||
|
||||
# Build and push (repository name separates your app)
|
||||
./scripts/build-and-push-azure.sh
|
||||
|
||||
# Images will be:
|
||||
# - zuyderlandacr.azurecr.io/cmdb-insight/backend:latest
|
||||
# - zuyderlandacr.azurecr.io/cmdb-insight/frontend:latest
|
||||
```
|
||||
|
||||
### Configure App Services
|
||||
|
||||
```bash
|
||||
# Backend App Service
|
||||
az webapp config container set \
|
||||
--name cmdb-backend-prod \
|
||||
--resource-group rg-cmdb-insight-prod \
|
||||
--docker-custom-image-name "zuyderlandacr.azurecr.io/cmdb-insight/backend:latest" \
|
||||
--docker-registry-server-url "https://zuyderlandacr.azurecr.io"
|
||||
|
||||
# Frontend App Service
|
||||
az webapp config container set \
|
||||
--name cmdb-frontend-prod \
|
||||
--resource-group rg-cmdb-insight-prod \
|
||||
--docker-custom-image-name "zuyderlandacr.azurecr.io/cmdb-insight/frontend:latest" \
|
||||
--docker-registry-server-url "https://zuyderlandacr.azurecr.io"
|
||||
```
|
||||
|
||||
### Update Pipeline Variables
|
||||
|
||||
In `azure-pipelines.yml`:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
acrName: 'yourcompanyacr' # Shared ACR name
|
||||
repositoryName: 'cmdb-insight' # Your app repository name
|
||||
# ... other variables
|
||||
```
|
||||
|
||||
## 🔐 Permissions
|
||||
|
||||
### Grant App Services Access to Shared ACR
|
||||
|
||||
```bash
|
||||
# Get App Service Managed Identity
|
||||
BACKEND_PRINCIPAL_ID=$(az webapp identity show \
|
||||
--name cmdb-backend-prod \
|
||||
--resource-group rg-cmdb-insight-prod \
|
||||
--query principalId --output tsv)
|
||||
|
||||
# Get ACR Resource ID (from shared resource group)
|
||||
ACR_ID=$(az acr show \
|
||||
--name zuyderlandacr \
|
||||
--resource-group rg-shared-services \
|
||||
--query id --output tsv)
|
||||
|
||||
# Grant AcrPull permission
|
||||
az role assignment create \
|
||||
--assignee $BACKEND_PRINCIPAL_ID \
|
||||
--role AcrPull \
|
||||
--scope $ACR_ID
|
||||
```
|
||||
|
||||
## 📊 Managing Multiple Applications
|
||||
|
||||
### List All Repositories
|
||||
|
||||
```bash
|
||||
# See all applications in ACR
|
||||
az acr repository list --name zuyderlandacr
|
||||
|
||||
# Output:
|
||||
# cmdb-insight
|
||||
# other-app
|
||||
# shared-services
|
||||
```
|
||||
|
||||
### List Images for This App
|
||||
|
||||
```bash
|
||||
# Backend images
|
||||
az acr repository show-tags \
|
||||
--name zuyderlandacr \
|
||||
--repository cmdb-insight/backend
|
||||
|
||||
# Frontend images
|
||||
az acr repository show-tags \
|
||||
--name zuyderlandacr \
|
||||
--repository cmdb-insight/frontend
|
||||
```
|
||||
|
||||
### Clean Up Old Images
|
||||
|
||||
```bash
|
||||
# Delete old tags (keep last 10)
|
||||
az acr repository show-tags \
|
||||
--name zuyderlandacr \
|
||||
--repository cmdb-insight/backend \
|
||||
--orderby time_desc \
|
||||
--query '[10:].name' \
|
||||
--output tsv | \
|
||||
xargs -I {} az acr repository delete \
|
||||
--name zuyderlandacr \
|
||||
--image cmdb-insight/backend:{} \
|
||||
--yes
|
||||
```
|
||||
|
||||
## 💰 Cost Optimization
|
||||
|
||||
### Shared ACR Costs
|
||||
|
||||
| SKU | Storage | Cost | Best For |
|
||||
|-----|---------|------|----------|
|
||||
| Basic | 10GB | €5/month | Small teams, few apps |
|
||||
| Standard | 100GB | €20/month | **Recommended for shared ACR** |
|
||||
| Premium | 500GB | €50/month | Large organizations |
|
||||
|
||||
**Recommendation**: Use **Standard** SKU for shared ACR:
|
||||
- Enough storage for multiple applications
|
||||
- Geo-replication available
|
||||
- Good balance of cost and features
|
||||
|
||||
### Cost Savings Example
|
||||
|
||||
**Without sharing:**
|
||||
- App 1 ACR: €20/month
|
||||
- App 2 ACR: €20/month
|
||||
- App 3 ACR: €20/month
|
||||
- **Total: €60/month**
|
||||
|
||||
**With shared ACR:**
|
||||
- Shared ACR (Standard): €20/month
|
||||
- **Total: €20/month**
|
||||
- **Savings: €40/month (67%)**
|
||||
|
||||
## 🎯 Best Practices
|
||||
|
||||
### 1. Naming Convention
|
||||
|
||||
Use consistent repository naming:
|
||||
- `app-name/service:tag` (e.g., `cmdb-insight/backend:latest`)
|
||||
- Avoid generic names like `backend`, `frontend`
|
||||
- Include app identifier in repository name
|
||||
|
||||
### 2. Resource Group Organization
|
||||
|
||||
**Option A: Separate Resource Groups**
|
||||
```
|
||||
rg-shared-services/
|
||||
└── ACR (shared)
|
||||
|
||||
rg-cmdb-insight-prod/
|
||||
└── App-specific resources
|
||||
```
|
||||
|
||||
**Option B: Single Resource Group**
|
||||
```
|
||||
rg-production/
|
||||
├── ACR
|
||||
├── App 1 resources
|
||||
├── App 2 resources
|
||||
└── App 3 resources
|
||||
```
|
||||
|
||||
### 3. Access Control
|
||||
|
||||
- Use **Managed Identity** for App Services (recommended)
|
||||
- Grant **AcrPull** role (not AcrPush) to App Services
|
||||
- Use **Service Principals** for CI/CD pipelines
|
||||
- Consider **Azure RBAC** for fine-grained access
|
||||
|
||||
### 4. Image Tagging Strategy
|
||||
|
||||
```bash
|
||||
# Use semantic versioning
|
||||
cmdb-insight/backend:v1.0.0
|
||||
cmdb-insight/backend:v1.0.1
|
||||
cmdb-insight/backend:latest
|
||||
|
||||
# Use build IDs for CI/CD
|
||||
cmdb-insight/backend:12345
|
||||
cmdb-insight/backend:latest
|
||||
```
|
||||
|
||||
## 🔄 Migration from Dedicated ACR
|
||||
|
||||
If you have a dedicated ACR and want to migrate to shared:
|
||||
|
||||
```bash
|
||||
# 1. Tag images with new repository name
|
||||
docker tag oldacr.azurecr.io/backend:latest newacr.azurecr.io/cmdb-insight/backend:latest
|
||||
docker tag oldacr.azurecr.io/frontend:latest newacr.azurecr.io/cmdb-insight/frontend:latest
|
||||
|
||||
# 2. Push to shared ACR
|
||||
docker push newacr.azurecr.io/cmdb-insight/backend:latest
|
||||
docker push newacr.azurecr.io/cmdb-insight/frontend:latest
|
||||
|
||||
# 3. Update App Services
|
||||
az webapp config container set \
|
||||
--name cmdb-backend-prod \
|
||||
--resource-group rg-cmdb-insight-prod \
|
||||
--docker-custom-image-name "newacr.azurecr.io/cmdb-insight/backend:latest"
|
||||
|
||||
# 4. Update pipeline variables
|
||||
# 5. Test deployment
|
||||
# 6. Delete old ACR (after verification)
|
||||
```
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **`AZURE-NEW-SUBSCRIPTION-SETUP.md`** - Complete Azure setup guide
|
||||
- **`AZURE-CONTAINER-REGISTRY.md`** - ACR setup and usage
|
||||
- **`AZURE-PIPELINE-DEPLOYMENT.md`** - Automated deployment
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] Decide: Use existing ACR or create new shared ACR
|
||||
- [ ] Verify ACR exists or create new one
|
||||
- [ ] Update pipeline variables with ACR name
|
||||
- [ ] Grant App Services access to ACR
|
||||
- [ ] Build and push images with repository name `cmdb-insight`
|
||||
- [ ] Configure App Services to use shared ACR
|
||||
- [ ] Test deployment
|
||||
- [ ] Document ACR name for team
|
||||
|
||||
---
|
||||
|
||||
**💡 Remember**: The repository name (`cmdb-insight`) is what separates your application from others in the shared ACR!
|
||||
943
docs/AZURE-NEW-SUBSCRIPTION-SETUP.md
Normal file
943
docs/AZURE-NEW-SUBSCRIPTION-SETUP.md
Normal file
@@ -0,0 +1,943 @@
|
||||
# Azure New Subscription Setup Guide
|
||||
|
||||
Complete guide for setting up all required Azure resources for CMDB Insight in a new Azure subscription.
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
This guide will help you create and configure all necessary Azure resources to deploy the CMDB Insight application. The setup includes:
|
||||
|
||||
### Required Resources
|
||||
|
||||
1. **Resource Group** - Container for all resources
|
||||
2. **Azure Container Registry (ACR)** - Store Docker images
|
||||
3. **Azure Database for PostgreSQL** - Production database (recommended)
|
||||
4. **Azure Key Vault** - Secure storage for secrets
|
||||
5. **Azure App Service Plan** - Hosting plan for web apps
|
||||
6. **Azure App Service (Backend)** - Backend API service
|
||||
7. **Azure App Service (Frontend)** - Frontend web application
|
||||
8. **Application Insights** - Monitoring and logging
|
||||
9. **DNS & SSL** - Custom domain and HTTPS certificate
|
||||
|
||||
### Estimated Costs
|
||||
|
||||
- **Basic Setup (SQLite)**: €17-35/month
|
||||
- **Recommended Setup (PostgreSQL)**: €36-62/month
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Prerequisites
|
||||
|
||||
Before starting, ensure you have:
|
||||
|
||||
- [ ] Azure CLI installed (`az --version`)
|
||||
- [ ] Azure subscription with appropriate permissions
|
||||
- [ ] Docker installed (for local testing)
|
||||
- [ ] Access to Azure Portal
|
||||
- [ ] Jira credentials (OAuth client ID/secret or Personal Access Token)
|
||||
|
||||
### Install Azure CLI (if needed)
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install azure-cli
|
||||
|
||||
# Linux
|
||||
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
||||
|
||||
# Windows
|
||||
# Download from: https://aka.ms/installazurecliwindows
|
||||
```
|
||||
|
||||
### Login to Azure
|
||||
|
||||
```bash
|
||||
az login
|
||||
az account list --output table
|
||||
az account set --subscription "<subscription-id-or-name>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Step 1: Create Resource Group
|
||||
|
||||
Create a resource group to organize all resources:
|
||||
|
||||
```bash
|
||||
# Set variables (customize as needed)
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
LOCATION="westeurope" # or your preferred region
|
||||
|
||||
# Create resource group
|
||||
az group create \
|
||||
--name $RESOURCE_GROUP \
|
||||
--location $LOCATION
|
||||
|
||||
# Verify
|
||||
az group show --name $RESOURCE_GROUP
|
||||
```
|
||||
|
||||
**Note**: Choose a location close to your users. Common options:
|
||||
- `westeurope` (Netherlands, Germany)
|
||||
- `northeurope` (Ireland, UK)
|
||||
- `eastus` (US East)
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Step 2: Create or Use Existing Azure Container Registry (ACR)
|
||||
|
||||
**Important**: Azure Container Registry can be **shared across multiple applications**. The repository name (`cmdb-insight`) is what separates this application from others in the same ACR.
|
||||
|
||||
### Option A: Use Existing ACR (Recommended if you have one)
|
||||
|
||||
If you already have an ACR for other applications, you can reuse it:
|
||||
|
||||
```bash
|
||||
# Set variables - use your existing ACR name
|
||||
ACR_NAME="your-existing-acr" # Your existing ACR name
|
||||
ACR_RESOURCE_GROUP="rg-shared-services" # Resource group where ACR exists
|
||||
|
||||
# Verify ACR exists
|
||||
az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP
|
||||
|
||||
# Get ACR login server
|
||||
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP --query loginServer --output tsv)
|
||||
echo "ACR Login Server: $ACR_LOGIN_SERVER"
|
||||
```
|
||||
|
||||
**Benefits of reusing ACR**:
|
||||
- ✅ Cost savings (one ACR for all apps)
|
||||
- ✅ Centralized image management
|
||||
- ✅ Easier to share images across teams
|
||||
- ✅ Better resource utilization
|
||||
|
||||
### Option B: Create New ACR
|
||||
|
||||
If you don't have an ACR yet, create one:
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
ACR_NAME="yourcompanyacr" # Must be globally unique, lowercase, 5-50 chars, alphanumeric only
|
||||
ACR_RESOURCE_GROUP="rg-shared-services" # Or use your app resource group
|
||||
SKU="Standard" # Options: Basic, Standard, Premium
|
||||
|
||||
# Create resource group for shared services (if needed)
|
||||
az group create --name $ACR_RESOURCE_GROUP --location westeurope
|
||||
|
||||
# Create ACR
|
||||
az acr create \
|
||||
--resource-group $ACR_RESOURCE_GROUP \
|
||||
--name $ACR_NAME \
|
||||
--sku $SKU \
|
||||
--admin-enabled true
|
||||
|
||||
# Get ACR login server
|
||||
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP --query loginServer --output tsv)
|
||||
echo "ACR Login Server: $ACR_LOGIN_SERVER"
|
||||
|
||||
# Get admin credentials (save these securely)
|
||||
az acr credential show --name $ACR_NAME
|
||||
```
|
||||
|
||||
**ACR SKU Comparison**:
|
||||
- **Basic**: €5/month - Development/test, 10GB storage
|
||||
- **Standard**: €20/month - Production, 100GB storage, geo-replication (recommended)
|
||||
- **Premium**: €50/month - Enterprise, 500GB storage, advanced security
|
||||
|
||||
**Repository Structure in ACR**:
|
||||
```
|
||||
yourcompanyacr.azurecr.io/
|
||||
├── cmdb-insight/ ← This application
|
||||
│ ├── backend:latest
|
||||
│ └── frontend:latest
|
||||
├── other-app/ ← Other applications
|
||||
│ ├── api:latest
|
||||
│ └── web:latest
|
||||
└── shared-services/ ← Shared images
|
||||
└── nginx:latest
|
||||
```
|
||||
|
||||
### Test ACR Connection
|
||||
|
||||
```bash
|
||||
# Login to ACR
|
||||
az acr login --name $ACR_NAME
|
||||
|
||||
# Verify
|
||||
az acr repository list --name $ACR_NAME
|
||||
|
||||
# List repositories (you'll see cmdb-insight after first push)
|
||||
az acr repository list --name $ACR_NAME --output table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Step 3: Create Azure Database for PostgreSQL
|
||||
|
||||
PostgreSQL is recommended for production. Alternatively, you can use SQLite with Azure Storage (see Step 3B).
|
||||
|
||||
### Step 3A: PostgreSQL (Recommended)
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
DB_SERVER_NAME="cmdb-postgres-prod" # Must be globally unique
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
DB_ADMIN_USER="cmdbadmin"
|
||||
DB_ADMIN_PASSWORD="<generate-secure-password>" # Use a strong password!
|
||||
DB_NAME="cmdb"
|
||||
SKU="Standard_B1ms" # Burstable tier, 1 vCore, 2GB RAM
|
||||
|
||||
# Generate secure password (save this!)
|
||||
DB_ADMIN_PASSWORD=$(openssl rand -base64 32)
|
||||
echo "Database Password: $DB_ADMIN_PASSWORD"
|
||||
|
||||
# Create PostgreSQL Flexible Server
|
||||
az postgres flexible-server create \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--name $DB_SERVER_NAME \
|
||||
--location westeurope \
|
||||
--admin-user $DB_ADMIN_USER \
|
||||
--admin-password $DB_ADMIN_PASSWORD \
|
||||
--sku-name $SKU \
|
||||
--tier Burstable \
|
||||
--storage-size 32 \
|
||||
--version 15 \
|
||||
--public-access 0.0.0.0 # Allow Azure services (restrict later if needed)
|
||||
|
||||
# Create database
|
||||
az postgres flexible-server db create \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--server-name $DB_SERVER_NAME \
|
||||
--database-name $DB_NAME
|
||||
|
||||
# Get connection string
|
||||
DB_CONNECTION_STRING="postgresql://${DB_ADMIN_USER}:${DB_ADMIN_PASSWORD}@${DB_SERVER_NAME}.postgres.database.azure.com:5432/${DB_NAME}?sslmode=require"
|
||||
echo "Database Connection String: $DB_CONNECTION_STRING"
|
||||
|
||||
# Save connection details securely
|
||||
echo "DB_HOST=${DB_SERVER_NAME}.postgres.database.azure.com" > .env.azure
|
||||
echo "DB_USER=${DB_ADMIN_USER}" >> .env.azure
|
||||
echo "DB_PASSWORD=${DB_ADMIN_PASSWORD}" >> .env.azure
|
||||
echo "DB_NAME=${DB_NAME}" >> .env.azure
|
||||
```
|
||||
|
||||
**PostgreSQL SKU Options**:
|
||||
- **Standard_B1ms**: €20-30/month - 1 vCore, 2GB RAM (recommended for 20 users)
|
||||
- **Standard_B2s**: €40-50/month - 2 vCores, 4GB RAM (for growth)
|
||||
- **Standard_D2s_v3**: €100+/month - 2 vCores, 8GB RAM (high performance)
|
||||
|
||||
### Step 3B: SQLite with Azure Storage (Alternative)
|
||||
|
||||
If you prefer to use SQLite (simpler, but less scalable):
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
STORAGE_ACCOUNT_NAME="cmdbstorage$(openssl rand -hex 4)" # Must be globally unique, lowercase
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
|
||||
# Create storage account
|
||||
az storage account create \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--name $STORAGE_ACCOUNT_NAME \
|
||||
--location westeurope \
|
||||
--sku Standard_LRS
|
||||
|
||||
# Get storage account key
|
||||
STORAGE_KEY=$(az storage account keys list \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--account-name $STORAGE_ACCOUNT_NAME \
|
||||
--query "[0].value" --output tsv)
|
||||
|
||||
echo "Storage Account: $STORAGE_ACCOUNT_NAME"
|
||||
echo "Storage Key: $STORAGE_KEY"
|
||||
```
|
||||
|
||||
**Note**: SQLite works but has limitations with concurrent users. PostgreSQL is recommended for production.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Step 4: Create Azure Key Vault
|
||||
|
||||
Key Vault securely stores secrets like API keys, passwords, and tokens.
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
KEY_VAULT_NAME="kv-cmdb-insight-prod" # Must be globally unique
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
|
||||
# Create Key Vault
|
||||
az keyvault create \
|
||||
--name $KEY_VAULT_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--location westeurope \
|
||||
--sku standard
|
||||
|
||||
# Verify
|
||||
az keyvault show --name $KEY_VAULT_NAME --resource-group $RESOURCE_GROUP
|
||||
```
|
||||
|
||||
### Add Secrets to Key Vault
|
||||
|
||||
```bash
|
||||
# Set your actual values
|
||||
JIRA_PAT="your-jira-personal-access-token"
|
||||
SESSION_SECRET=$(openssl rand -hex 32)
|
||||
JIRA_OAUTH_CLIENT_ID="your-oauth-client-id"
|
||||
JIRA_OAUTH_CLIENT_SECRET="your-oauth-client-secret"
|
||||
JIRA_SCHEMA_ID="your-schema-id"
|
||||
|
||||
# Add secrets
|
||||
az keyvault secret set \
|
||||
--vault-name $KEY_VAULT_NAME \
|
||||
--name "JiraPat" \
|
||||
--value "$JIRA_PAT"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name $KEY_VAULT_NAME \
|
||||
--name "SessionSecret" \
|
||||
--value "$SESSION_SECRET"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name $KEY_VAULT_NAME \
|
||||
--name "JiraOAuthClientId" \
|
||||
--value "$JIRA_OAUTH_CLIENT_ID"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name $KEY_VAULT_NAME \
|
||||
--name "JiraOAuthClientSecret" \
|
||||
--value "$JIRA_OAUTH_CLIENT_SECRET"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name $KEY_VAULT_NAME \
|
||||
--name "JiraSchemaId" \
|
||||
--value "$JIRA_SCHEMA_ID"
|
||||
|
||||
# If using PostgreSQL, add database password
|
||||
az keyvault secret set \
|
||||
--vault-name $KEY_VAULT_NAME \
|
||||
--name "DatabasePassword" \
|
||||
--value "$DB_ADMIN_PASSWORD"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Step 5: Create Application Insights
|
||||
|
||||
Application Insights provides monitoring, logging, and performance metrics.
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
APP_INSIGHTS_NAME="appi-cmdb-insight-prod"
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
|
||||
# Create Application Insights
|
||||
az monitor app-insights component create \
|
||||
--app $APP_INSIGHTS_NAME \
|
||||
--location westeurope \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--application-type web
|
||||
|
||||
# Get Instrumentation Key
|
||||
INSTRUMENTATION_KEY=$(az monitor app-insights component show \
|
||||
--app $APP_INSIGHTS_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--query instrumentationKey --output tsv)
|
||||
|
||||
echo "Instrumentation Key: $INSTRUMENTATION_KEY"
|
||||
```
|
||||
|
||||
**Note**: Application Insights Basic tier is free up to 5GB/month, which is sufficient for most small applications.
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Step 6: Create App Service Plan
|
||||
|
||||
App Service Plan defines the compute resources for your web apps.
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
APP_SERVICE_PLAN_NAME="plan-cmdb-insight-prod"
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
SKU="B1" # Basic tier, 1 vCore, 1.75GB RAM
|
||||
|
||||
# Create App Service Plan (Linux)
|
||||
az appservice plan create \
|
||||
--name $APP_SERVICE_PLAN_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--sku $SKU \
|
||||
--is-linux
|
||||
|
||||
# Verify
|
||||
az appservice plan show --name $APP_SERVICE_PLAN_NAME --resource-group $RESOURCE_GROUP
|
||||
```
|
||||
|
||||
**App Service Plan SKU Options**:
|
||||
- **B1**: €15-25/month - 1 vCore, 1.75GB RAM (recommended for 20 users)
|
||||
- **B2**: €30-40/month - 2 vCores, 3.5GB RAM
|
||||
- **S1**: €50-70/month - 1 vCore, 1.75GB RAM (Standard tier, better performance)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Step 7: Create App Services (Backend & Frontend)
|
||||
|
||||
Create two web apps: one for backend API and one for frontend.
|
||||
|
||||
### Step 7A: Create Backend App Service
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
BACKEND_APP_NAME="cmdb-backend-prod" # Must be globally unique
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
APP_SERVICE_PLAN_NAME="plan-cmdb-insight-prod"
|
||||
ACR_NAME="cmdbinsightacr" # From Step 2
|
||||
|
||||
# Create backend web app
|
||||
az webapp create \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--plan $APP_SERVICE_PLAN_NAME \
|
||||
--deployment-container-image-name "${ACR_NAME}.azurecr.io/cmdb-insight/backend:latest"
|
||||
|
||||
# Enable Managed Identity (for ACR access)
|
||||
az webapp identity assign \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP
|
||||
|
||||
# Get Managed Identity Principal ID
|
||||
BACKEND_PRINCIPAL_ID=$(az webapp identity show \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--query principalId --output tsv)
|
||||
|
||||
# Grant ACR pull permissions
|
||||
ACR_ID=$(az acr show --name $ACR_NAME --resource-group $RESOURCE_GROUP --query id --output tsv)
|
||||
az role assignment create \
|
||||
--assignee $BACKEND_PRINCIPAL_ID \
|
||||
--role AcrPull \
|
||||
--scope $ACR_ID
|
||||
|
||||
# Configure container settings
|
||||
az webapp config container set \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--docker-custom-image-name "${ACR_NAME}.azurecr.io/cmdb-insight/backend:latest" \
|
||||
--docker-registry-server-url "https://${ACR_NAME}.azurecr.io"
|
||||
|
||||
# Set environment variables
|
||||
az webapp config appsettings set \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--settings \
|
||||
NODE_ENV=production \
|
||||
PORT=3001 \
|
||||
DATABASE_TYPE=postgres \
|
||||
DATABASE_URL="@Microsoft.KeyVault(SecretUri=https://${KEY_VAULT_NAME}.vault.azure.net/secrets/DatabasePassword/)" \
|
||||
JIRA_HOST=https://jira.zuyderland.nl \
|
||||
JIRA_AUTH_METHOD=oauth \
|
||||
JIRA_OAUTH_CLIENT_ID="@Microsoft.KeyVault(SecretUri=https://${KEY_VAULT_NAME}.vault.azure.net/secrets/JiraOAuthClientId/)" \
|
||||
JIRA_OAUTH_CLIENT_SECRET="@Microsoft.KeyVault(SecretUri=https://${KEY_VAULT_NAME}.vault.azure.net/secrets/JiraOAuthClientSecret/)" \
|
||||
JIRA_OAUTH_CALLBACK_URL="https://${BACKEND_APP_NAME}.azurewebsites.net/api/auth/callback" \
|
||||
JIRA_SCHEMA_ID="@Microsoft.KeyVault(SecretUri=https://${KEY_VAULT_NAME}.vault.azure.net/secrets/JiraSchemaId/)" \
|
||||
SESSION_SECRET="@Microsoft.KeyVault(SecretUri=https://${KEY_VAULT_NAME}.vault.azure.net/secrets/SessionSecret/)" \
|
||||
FRONTEND_URL="https://${FRONTEND_APP_NAME}.azurewebsites.net" \
|
||||
APPINSIGHTS_INSTRUMENTATIONKEY="${INSTRUMENTATION_KEY}"
|
||||
|
||||
# Grant Key Vault access to backend
|
||||
az keyvault set-policy \
|
||||
--name $KEY_VAULT_NAME \
|
||||
--object-id $BACKEND_PRINCIPAL_ID \
|
||||
--secret-permissions get list
|
||||
|
||||
# Enable HTTPS only
|
||||
az webapp update \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--https-only true
|
||||
```
|
||||
|
||||
### Step 7B: Create Frontend App Service
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
FRONTEND_APP_NAME="cmdb-frontend-prod" # Must be globally unique
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
APP_SERVICE_PLAN_NAME="plan-cmdb-insight-prod"
|
||||
ACR_NAME="cmdbinsightacr" # From Step 2
|
||||
|
||||
# Create frontend web app
|
||||
az webapp create \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--plan $APP_SERVICE_PLAN_NAME \
|
||||
--deployment-container-image-name "${ACR_NAME}.azurecr.io/cmdb-insight/frontend:latest"
|
||||
|
||||
# Enable Managed Identity
|
||||
az webapp identity assign \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP
|
||||
|
||||
# Get Managed Identity Principal ID
|
||||
FRONTEND_PRINCIPAL_ID=$(az webapp identity show \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--query principalId --output tsv)
|
||||
|
||||
# Grant ACR pull permissions
|
||||
az role assignment create \
|
||||
--assignee $FRONTEND_PRINCIPAL_ID \
|
||||
--role AcrPull \
|
||||
--scope $ACR_ID
|
||||
|
||||
# Configure container settings
|
||||
az webapp config container set \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--docker-custom-image-name "${ACR_NAME}.azurecr.io/cmdb-insight/frontend:latest" \
|
||||
--docker-registry-server-url "https://${ACR_NAME}.azurecr.io"
|
||||
|
||||
# Set environment variables
|
||||
az webapp config appsettings set \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--settings \
|
||||
VITE_API_URL="https://${BACKEND_APP_NAME}.azurewebsites.net/api"
|
||||
|
||||
# Enable HTTPS only
|
||||
az webapp update \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--https-only true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Step 8: Build and Push Docker Images
|
||||
|
||||
Before the apps can run, you need to build and push Docker images to ACR.
|
||||
|
||||
### Option A: Using Script (Recommended)
|
||||
|
||||
```bash
|
||||
# Navigate to project root
|
||||
cd /path/to/cmdb-insight
|
||||
|
||||
# Set environment variables
|
||||
export ACR_NAME="cmdbinsightacr" # Your ACR name
|
||||
export REPO_NAME="cmdb-insight"
|
||||
|
||||
# Build and push
|
||||
./scripts/build-and-push-azure.sh
|
||||
```
|
||||
|
||||
### Option B: Manual Build and Push
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
ACR_NAME="cmdbinsightacr"
|
||||
REGISTRY="${ACR_NAME}.azurecr.io"
|
||||
REPO_NAME="cmdb-insight"
|
||||
|
||||
# Login to ACR
|
||||
az acr login --name $ACR_NAME
|
||||
|
||||
# Build backend
|
||||
docker build -t ${REGISTRY}/${REPO_NAME}/backend:latest \
|
||||
-f backend/Dockerfile.prod ./backend
|
||||
|
||||
# Build frontend
|
||||
docker build -t ${REGISTRY}/${REPO_NAME}/frontend:latest \
|
||||
-f frontend/Dockerfile.prod ./frontend
|
||||
|
||||
# Push images
|
||||
docker push ${REGISTRY}/${REPO_NAME}/backend:latest
|
||||
docker push ${REGISTRY}/${REPO_NAME}/frontend:latest
|
||||
|
||||
# Verify
|
||||
az acr repository list --name $ACR_NAME
|
||||
az acr repository show-tags --name $ACR_NAME --repository ${REPO_NAME}/backend
|
||||
az acr repository show-tags --name $ACR_NAME --repository ${REPO_NAME}/frontend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Step 9: Configure Custom Domain and SSL (Optional)
|
||||
|
||||
If you have a custom domain (e.g., `cmdb.yourcompany.com`):
|
||||
|
||||
### Step 9A: Add Custom Domain
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
FRONTEND_APP_NAME="cmdb-frontend-prod"
|
||||
BACKEND_APP_NAME="cmdb-backend-prod"
|
||||
CUSTOM_DOMAIN="cmdb.yourcompany.com"
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
|
||||
# Add custom domain to frontend
|
||||
az webapp config hostname add \
|
||||
--webapp-name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--hostname $CUSTOM_DOMAIN
|
||||
|
||||
# Add custom domain to backend (if needed)
|
||||
az webapp config hostname add \
|
||||
--webapp-name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--hostname "api.${CUSTOM_DOMAIN}"
|
||||
```
|
||||
|
||||
### Step 9B: Configure SSL Certificate
|
||||
|
||||
**Option 1: App Service Managed Certificate (Free, Recommended)**
|
||||
|
||||
```bash
|
||||
# Create managed certificate for frontend
|
||||
az webapp config ssl create \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--hostname $CUSTOM_DOMAIN
|
||||
|
||||
# Bind certificate
|
||||
az webapp config ssl bind \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--certificate-thumbprint <thumbprint> \
|
||||
--ssl-type SNI
|
||||
```
|
||||
|
||||
**Option 2: Import Existing Certificate**
|
||||
|
||||
```bash
|
||||
# Upload certificate
|
||||
az webapp config ssl upload \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--certificate-file /path/to/certificate.pfx \
|
||||
--certificate-password <password>
|
||||
```
|
||||
|
||||
**Note**: You'll need to update DNS records to point to your App Service. Get the IP address:
|
||||
|
||||
```bash
|
||||
az webapp show --name $FRONTEND_APP_NAME --resource-group $RESOURCE_GROUP --query defaultHostName
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Step 10: Verify Deployment
|
||||
|
||||
### Check App Status
|
||||
|
||||
```bash
|
||||
# Check backend
|
||||
az webapp show --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP --query state
|
||||
|
||||
# Check frontend
|
||||
az webapp show --name $FRONTEND_APP_NAME --resource-group $RESOURCE_GROUP --query state
|
||||
|
||||
# Start apps if needed
|
||||
az webapp start --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP
|
||||
az webapp start --name $FRONTEND_APP_NAME --resource-group $RESOURCE_GROUP
|
||||
```
|
||||
|
||||
### Test Health Endpoints
|
||||
|
||||
```bash
|
||||
# Backend health check
|
||||
curl https://${BACKEND_APP_NAME}.azurewebsites.net/api/health
|
||||
|
||||
# Frontend
|
||||
curl https://${FRONTEND_APP_NAME}.azurewebsites.net
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# Backend logs
|
||||
az webapp log tail --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP
|
||||
|
||||
# Frontend logs
|
||||
az webapp log tail --name $FRONTEND_APP_NAME --resource-group $RESOURCE_GROUP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Step 11: Configure Database Schema
|
||||
|
||||
If using PostgreSQL, you need to initialize the database schema:
|
||||
|
||||
```bash
|
||||
# Connect to database and run schema initialization
|
||||
# Option 1: Using psql
|
||||
psql "postgresql://${DB_ADMIN_USER}:${DB_ADMIN_PASSWORD}@${DB_SERVER_NAME}.postgres.database.azure.com:5432/${DB_NAME}?sslmode=require"
|
||||
|
||||
# Option 2: Using Azure Cloud Shell or local script
|
||||
# The application will create tables automatically on first run
|
||||
# Or use the migration scripts in backend/scripts/
|
||||
```
|
||||
|
||||
**Note**: The application will automatically create the required database schema on first startup if it doesn't exist.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Step 12: Update Environment Variables (If Needed)
|
||||
|
||||
If you need to update any configuration:
|
||||
|
||||
```bash
|
||||
# Update backend settings
|
||||
az webapp config appsettings set \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--settings \
|
||||
NEW_SETTING=value
|
||||
|
||||
# Update frontend settings
|
||||
az webapp config appsettings set \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--settings \
|
||||
VITE_API_URL="https://${BACKEND_APP_NAME}.azurewebsites.net/api"
|
||||
|
||||
# Restart apps to apply changes
|
||||
az webapp restart --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP
|
||||
az webapp restart --name $FRONTEND_APP_NAME --resource-group $RESOURCE_GROUP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Step 13: Set Up CI/CD with Automated Deployment
|
||||
|
||||
### Azure DevOps Pipeline Setup
|
||||
|
||||
The project includes an automated pipeline that builds, pushes, and deploys your application.
|
||||
|
||||
1. **Create Service Connections**:
|
||||
|
||||
**A) Docker Registry Connection (for building images)**:
|
||||
- Go to Azure DevOps → Project Settings → Service connections
|
||||
- Create new **Docker Registry** connection
|
||||
- Select **Azure Container Registry**
|
||||
- Choose your subscription and ACR
|
||||
- Name: `zuyderland-cmdb-acr-connection` (or match your variable)
|
||||
|
||||
**B) Azure Resource Manager Connection (for deployment)**:
|
||||
- Create new **Azure Resource Manager** connection
|
||||
- Select your subscription
|
||||
- Name: `zuyderland-cmdb-subscription` (or match your variable)
|
||||
|
||||
2. **Configure Pipeline Variables**:
|
||||
|
||||
Update `azure-pipelines.yml` with your values:
|
||||
```yaml
|
||||
variables:
|
||||
acrName: 'cmdbinsightacr' # Your ACR name
|
||||
resourceGroup: 'rg-cmdb-insight-prod' # Your resource group
|
||||
backendAppName: 'cmdb-backend-prod' # Your backend app name
|
||||
frontendAppName: 'cmdb-frontend-prod' # Your frontend app name
|
||||
azureSubscription: 'zuyderland-cmdb-subscription' # Azure service connection
|
||||
dockerRegistryServiceConnection: 'zuyderland-cmdb-acr-connection'
|
||||
```
|
||||
|
||||
3. **Create Environment**:
|
||||
- Go to **Pipelines** → **Environments**
|
||||
- Create environment: `production`
|
||||
- (Optional) Add approvals for manual deployment control
|
||||
|
||||
4. **Run Pipeline**:
|
||||
- Push to `main` branch → **Automatically builds AND deploys**
|
||||
- Pipeline will:
|
||||
- Build Docker images
|
||||
- Push to ACR
|
||||
- Deploy to App Services
|
||||
- Verify deployment
|
||||
|
||||
### Zero-Downtime Deployment (Optional)
|
||||
|
||||
For production with zero downtime, use deployment slots:
|
||||
|
||||
1. **Create Staging Slots**:
|
||||
```bash
|
||||
az webapp deployment slot create \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--slot staging
|
||||
|
||||
az webapp deployment slot create \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--slot staging
|
||||
```
|
||||
|
||||
2. **Use Advanced Pipeline**:
|
||||
- Use `azure-pipelines-slots.yml` instead
|
||||
- Deploys to staging first
|
||||
- Swaps to production after verification
|
||||
|
||||
**See `docs/AZURE-PIPELINE-DEPLOYMENT.md` for complete setup guide.**
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### App Service Won't Start
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
az webapp log tail --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP
|
||||
|
||||
# Check container logs
|
||||
az webapp log show --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP
|
||||
|
||||
# Check app status
|
||||
az webapp show --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP --query state
|
||||
```
|
||||
|
||||
### ACR Authentication Issues
|
||||
|
||||
```bash
|
||||
# Re-authenticate
|
||||
az acr login --name $ACR_NAME
|
||||
|
||||
# Check Managed Identity permissions
|
||||
az role assignment list --assignee $BACKEND_PRINCIPAL_ID --scope $ACR_ID
|
||||
```
|
||||
|
||||
### Database Connection Issues
|
||||
|
||||
```bash
|
||||
# Test database connection
|
||||
psql "postgresql://${DB_ADMIN_USER}:${DB_ADMIN_PASSWORD}@${DB_SERVER_NAME}.postgres.database.azure.com:5432/${DB_NAME}?sslmode=require"
|
||||
|
||||
# Check firewall rules
|
||||
az postgres flexible-server firewall-rule list \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--name $DB_SERVER_NAME
|
||||
```
|
||||
|
||||
### Key Vault Access Issues
|
||||
|
||||
```bash
|
||||
# Check Key Vault policies
|
||||
az keyvault show --name $KEY_VAULT_NAME --resource-group $RESOURCE_GROUP
|
||||
|
||||
# Verify Managed Identity has access
|
||||
az keyvault show-policy --name $KEY_VAULT_NAME
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Application Insights
|
||||
|
||||
1. Go to Azure Portal → Application Insights → Your app
|
||||
2. View:
|
||||
- **Live Metrics**: Real-time performance
|
||||
- **Application Map**: Service dependencies
|
||||
- **Logs**: Query application logs
|
||||
- **Metrics**: Performance metrics
|
||||
|
||||
### Set Up Alerts
|
||||
|
||||
```bash
|
||||
# Create alert for app downtime
|
||||
az monitor metrics alert create \
|
||||
--name "Backend-Down" \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--scopes "/subscriptions/<subscription-id>/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Web/sites/${BACKEND_APP_NAME}" \
|
||||
--condition "avg AvailabilityPercentage < 99" \
|
||||
--window-size 5m \
|
||||
--evaluation-frequency 1m
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💰 Cost Optimization
|
||||
|
||||
### Current Setup Costs
|
||||
|
||||
| Resource | SKU | Estimated Monthly Cost |
|
||||
|----------|-----|------------------------|
|
||||
| App Service Plan | B1 | €15-25 |
|
||||
| PostgreSQL | Standard_B1ms | €20-30 |
|
||||
| Container Registry | Basic | €5 |
|
||||
| Key Vault | Standard | €1-2 |
|
||||
| Application Insights | Basic | €0-5 (free tier) |
|
||||
| **Total** | | **€41-67/month** |
|
||||
|
||||
### Cost Saving Tips
|
||||
|
||||
1. **Use Basic tier ACR** for development (€5 vs €20)
|
||||
2. **Application Insights Basic** is free up to 5GB/month
|
||||
3. **Stop App Services** when not in use (dev/test environments)
|
||||
4. **Use SQLite** instead of PostgreSQL (saves €20-30/month, but less scalable)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Configure DNS**: Point your domain to App Service
|
||||
2. **Set up SSL**: Configure HTTPS certificate
|
||||
3. **Test Application**: Verify all features work
|
||||
4. **Set up Monitoring**: Configure alerts
|
||||
5. **Document Access**: Share URLs and credentials with team
|
||||
6. **Backup Strategy**: Plan for database backups (if needed)
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Useful Commands Reference
|
||||
|
||||
```bash
|
||||
# List all resources
|
||||
az resource list --resource-group $RESOURCE_GROUP --output table
|
||||
|
||||
# Get resource IDs
|
||||
az acr show --name $ACR_NAME --resource-group $RESOURCE_GROUP --query id
|
||||
az postgres flexible-server show --name $DB_SERVER_NAME --resource-group $RESOURCE_GROUP --query id
|
||||
|
||||
# Export resource configuration
|
||||
az group export --name $RESOURCE_GROUP --output-file resources.json
|
||||
|
||||
# Delete all resources (careful!)
|
||||
az group delete --name $RESOURCE_GROUP --yes --no-wait
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Additional Resources
|
||||
|
||||
- [Azure App Service Documentation](https://docs.microsoft.com/azure/app-service/)
|
||||
- [Azure Container Registry Documentation](https://docs.microsoft.com/azure/container-registry/)
|
||||
- [Azure Database for PostgreSQL Documentation](https://docs.microsoft.com/azure/postgresql/)
|
||||
- [Azure Key Vault Documentation](https://docs.microsoft.com/azure/key-vault/)
|
||||
- [Application Insights Documentation](https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Deployment Checklist
|
||||
|
||||
- [ ] Resource Group created
|
||||
- [ ] Azure Container Registry created and accessible
|
||||
- [ ] PostgreSQL database created (or SQLite storage configured)
|
||||
- [ ] Key Vault created with all secrets
|
||||
- [ ] Application Insights created
|
||||
- [ ] App Service Plan created
|
||||
- [ ] Backend App Service created and configured
|
||||
- [ ] Frontend App Service created and configured
|
||||
- [ ] Docker images built and pushed to ACR
|
||||
- [ ] Apps started and running
|
||||
- [ ] Health checks passing
|
||||
- [ ] Custom domain configured (if applicable)
|
||||
- [ ] SSL certificate configured (if applicable)
|
||||
- [ ] Monitoring and alerts configured
|
||||
- [ ] Team access configured
|
||||
- [ ] Documentation updated
|
||||
|
||||
---
|
||||
|
||||
**🎉 Congratulations! Your CMDB Insight application is now deployed to Azure!**
|
||||
|
||||
For questions or issues, refer to:
|
||||
- `AZURE-APP-SERVICE-DEPLOYMENT.md` - Detailed App Service deployment guide
|
||||
- `AZURE-CONTAINER-REGISTRY.md` - ACR setup and usage
|
||||
- `PRODUCTION-DEPLOYMENT.md` - General production deployment guide
|
||||
337
docs/AZURE-PIPELINE-DEPLOYMENT.md
Normal file
337
docs/AZURE-PIPELINE-DEPLOYMENT.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Azure Pipeline Automated Deployment Guide
|
||||
|
||||
Complete guide for setting up automated deployment from Azure DevOps Pipeline to Azure App Services.
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
The enhanced `azure-pipelines.yml` now includes:
|
||||
- ✅ **Build Stage**: Builds and pushes Docker images to ACR
|
||||
- ✅ **Deploy Stage**: Automatically deploys to Azure App Services
|
||||
- ✅ **Verification**: Health checks after deployment
|
||||
|
||||
## 🚀 Quick Setup
|
||||
|
||||
### Step 1: Configure Pipeline Variables
|
||||
|
||||
Update the variables in `azure-pipelines.yml`:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
# Azure Container Registry
|
||||
acrName: 'zdlas' # Your ACR name
|
||||
repositoryName: 'cmdb-insight'
|
||||
dockerRegistryServiceConnection: 'zuyderland-cmdb-acr-connection'
|
||||
|
||||
# Azure App Service
|
||||
resourceGroup: 'rg-cmdb-insight-prod' # Your resource group
|
||||
backendAppName: 'cmdb-backend-prod' # Your backend app name
|
||||
frontendAppName: 'cmdb-frontend-prod' # Your frontend app name
|
||||
azureSubscription: 'zuyderland-cmdb-subscription' # Azure service connection
|
||||
|
||||
# Deployment
|
||||
deployToProduction: true # Set false to skip deployment
|
||||
useDeploymentSlots: false # Set true for zero-downtime deployment
|
||||
```
|
||||
|
||||
### Step 2: Create Azure Service Connection
|
||||
|
||||
You need an Azure service connection for App Service deployment:
|
||||
|
||||
1. **Go to Azure DevOps** → Your Project
|
||||
2. **Project Settings** → **Service connections** → **New service connection**
|
||||
3. Choose **Azure Resource Manager**
|
||||
4. Select:
|
||||
- **Authentication method**: Managed identity (recommended) or Service principal
|
||||
- **Azure subscription**: Your subscription
|
||||
- **Resource group**: Your resource group (optional)
|
||||
5. **Service connection name**: `zuyderland-cmdb-subscription` (match the variable name)
|
||||
6. Click **Save**
|
||||
|
||||
### Step 3: Configure Environment
|
||||
|
||||
The pipeline uses an `environment` called `production`:
|
||||
|
||||
1. **Go to Pipelines** → **Environments**
|
||||
2. Click **Create environment**
|
||||
3. Name: `production`
|
||||
4. Add **Approvals and checks** (optional):
|
||||
- **Approvals**: Require manual approval before deployment
|
||||
- **Gate**: Health checks before deployment
|
||||
|
||||
### Step 4: Run Pipeline
|
||||
|
||||
The pipeline will automatically:
|
||||
1. Build Docker images
|
||||
2. Push to ACR
|
||||
3. Deploy to App Services
|
||||
4. Verify deployment
|
||||
|
||||
**Trigger automatically on:**
|
||||
- Push to `main` branch
|
||||
- Git tags starting with `v*`
|
||||
|
||||
## 🔧 Configuration Options
|
||||
|
||||
### Enable/Disable Deployment
|
||||
|
||||
To skip deployment (only build images):
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
deployToProduction: false
|
||||
```
|
||||
|
||||
Or use pipeline variables in Azure DevOps:
|
||||
1. Go to **Pipelines** → Your pipeline → **Edit**
|
||||
2. Click **Variables**
|
||||
3. Add variable: `deployToProduction` = `false`
|
||||
|
||||
### Use Specific Image Tag
|
||||
|
||||
By default, the pipeline deploys the `latest` tag. To deploy a specific version:
|
||||
|
||||
```yaml
|
||||
# In the Deploy stage, change:
|
||||
containers: '$(acrName).azurecr.io/$(repositoryName)/backend:$(imageTag)'
|
||||
```
|
||||
|
||||
This will deploy the specific build ID instead of `latest`.
|
||||
|
||||
## 🎯 Zero-Downtime Deployment (Deployment Slots)
|
||||
|
||||
For production deployments without downtime, use deployment slots:
|
||||
|
||||
### Step 1: Create Deployment Slots
|
||||
|
||||
```bash
|
||||
# Create staging slot for backend
|
||||
az webapp deployment slot create \
|
||||
--name cmdb-backend-prod \
|
||||
--resource-group rg-cmdb-insight-prod \
|
||||
--slot staging
|
||||
|
||||
# Create staging slot for frontend
|
||||
az webapp deployment slot create \
|
||||
--name cmdb-frontend-prod \
|
||||
--resource-group rg-cmdb-insight-prod \
|
||||
--slot staging
|
||||
```
|
||||
|
||||
### Step 2: Update Pipeline for Slots
|
||||
|
||||
Create `azure-pipelines-slots.yml` (see advanced example below) or modify the existing pipeline:
|
||||
|
||||
```yaml
|
||||
- task: AzureWebAppContainer@1
|
||||
displayName: 'Deploy to Staging Slot'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
appName: '$(backendAppName)'
|
||||
deployToSlotOrASE: true
|
||||
resourceGroupName: '$(resourceGroup)'
|
||||
slotName: 'staging'
|
||||
containers: '$(acrName).azurecr.io/$(repositoryName)/backend:latest'
|
||||
|
||||
- task: AzureCLI@2
|
||||
displayName: 'Swap Staging to Production'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
az webapp deployment slot swap \
|
||||
--name $(backendAppName) \
|
||||
--resource-group $(resourceGroup) \
|
||||
--slot staging \
|
||||
--target-slot production
|
||||
```
|
||||
|
||||
## 📊 Pipeline Stages
|
||||
|
||||
### Stage 1: Build
|
||||
|
||||
**What it does:**
|
||||
- Builds backend Docker image
|
||||
- Builds frontend Docker image
|
||||
- Pushes both to ACR with tags: `$(Build.BuildId)` and `latest`
|
||||
|
||||
**Output:**
|
||||
- `backendImage`: Full image URL for backend
|
||||
- `frontendImage`: Full image URL for frontend
|
||||
|
||||
### Stage 2: Deploy
|
||||
|
||||
**What it does:**
|
||||
- Deploys backend container to App Service
|
||||
- Deploys frontend container to App Service
|
||||
- Restarts both App Services
|
||||
- Verifies deployment with health checks
|
||||
|
||||
**Conditions:**
|
||||
- Only runs if `deployToProduction = true`
|
||||
- Only runs if Build stage succeeded
|
||||
|
||||
### Stage 3: Verify
|
||||
|
||||
**What it does:**
|
||||
- Checks backend health endpoint (`/api/health`)
|
||||
- Checks frontend accessibility
|
||||
- Reports status
|
||||
|
||||
## 🔐 Permissions Required
|
||||
|
||||
The Azure service connection needs:
|
||||
|
||||
1. **App Service Contributor** role on:
|
||||
- Backend App Service
|
||||
- Frontend App Service
|
||||
- App Service Plan
|
||||
|
||||
2. **ACR Pull** permissions (if using Managed Identity):
|
||||
- Already configured via Managed Identity on App Services
|
||||
|
||||
### Grant Permissions
|
||||
|
||||
```bash
|
||||
# Get service principal ID from Azure DevOps service connection
|
||||
# Then grant permissions:
|
||||
|
||||
az role assignment create \
|
||||
--assignee <service-principal-id> \
|
||||
--role "Website Contributor" \
|
||||
--scope /subscriptions/<subscription-id>/resourceGroups/rg-cmdb-insight-prod
|
||||
```
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Deployment Fails: "Service connection not found"
|
||||
|
||||
**Solution:**
|
||||
- Verify service connection name matches `azureSubscription` variable
|
||||
- Check service connection exists in Project Settings → Service connections
|
||||
- Verify service connection has correct permissions
|
||||
|
||||
### Deployment Fails: "App Service not found"
|
||||
|
||||
**Solution:**
|
||||
- Verify `backendAppName` and `frontendAppName` variables are correct
|
||||
- Check `resourceGroup` variable matches your resource group
|
||||
- Verify App Services exist in Azure
|
||||
|
||||
### Images Not Updating
|
||||
|
||||
**Solution:**
|
||||
- Check if images were pushed to ACR successfully
|
||||
- Verify App Service is pulling from correct ACR
|
||||
- Check container settings in App Service configuration
|
||||
- Ensure Managed Identity has ACR pull permissions
|
||||
|
||||
### Health Check Fails
|
||||
|
||||
**Solution:**
|
||||
- Wait longer (apps may need time to start)
|
||||
- Check App Service logs: `az webapp log tail`
|
||||
- Verify health endpoint exists: `/api/health`
|
||||
- Check environment variables are configured correctly
|
||||
|
||||
## 📝 Manual Deployment (Alternative)
|
||||
|
||||
If you prefer manual deployment after pipeline builds:
|
||||
|
||||
```bash
|
||||
# After pipeline builds images, manually deploy:
|
||||
|
||||
# Restart backend to pull latest image
|
||||
az webapp restart \
|
||||
--name cmdb-backend-prod \
|
||||
--resource-group rg-cmdb-insight-prod
|
||||
|
||||
# Restart frontend to pull latest image
|
||||
az webapp restart \
|
||||
--name cmdb-frontend-prod \
|
||||
--resource-group rg-cmdb-insight-prod
|
||||
```
|
||||
|
||||
## 🎯 Best Practices
|
||||
|
||||
### 1. Use Deployment Slots for Production
|
||||
|
||||
- Deploy to staging slot first
|
||||
- Test in staging
|
||||
- Swap to production when ready
|
||||
|
||||
### 2. Use Specific Tags for Production
|
||||
|
||||
Instead of `latest`, use version tags:
|
||||
```yaml
|
||||
containers: '$(acrName).azurecr.io/$(repositoryName)/backend:v1.0.0'
|
||||
```
|
||||
|
||||
### 3. Add Approvals for Production
|
||||
|
||||
Configure environment approvals:
|
||||
- Go to **Pipelines** → **Environments** → **production**
|
||||
- Add **Approvals** check
|
||||
- Require manual approval before deployment
|
||||
|
||||
### 4. Monitor Deployments
|
||||
|
||||
- Set up alerts in Application Insights
|
||||
- Monitor pipeline runs
|
||||
- Check deployment logs regularly
|
||||
|
||||
### 5. Rollback Strategy
|
||||
|
||||
If deployment fails:
|
||||
```bash
|
||||
# Rollback to previous image
|
||||
az webapp config container set \
|
||||
--name cmdb-backend-prod \
|
||||
--resource-group rg-cmdb-insight-prod \
|
||||
--docker-custom-image-name <previous-image-tag>
|
||||
```
|
||||
|
||||
## 🔄 Workflow Example
|
||||
|
||||
### Typical Development Workflow
|
||||
|
||||
1. **Developer pushes code** to `main` branch
|
||||
2. **Pipeline triggers automatically**
|
||||
3. **Build stage**: Builds and pushes images
|
||||
4. **Deploy stage**: Deploys to App Services
|
||||
5. **Verify stage**: Checks health
|
||||
6. **Application updated** - ready to use!
|
||||
|
||||
### Release Workflow
|
||||
|
||||
1. **Create release tag**: `git tag v1.0.0 && git push origin v1.0.0`
|
||||
2. **Pipeline triggers** with tag
|
||||
3. **Build stage**: Builds versioned images (`v1.0.0`)
|
||||
4. **Deploy stage**: Deploys to staging slot
|
||||
5. **Manual approval** (if configured)
|
||||
6. **Swap to production**: Zero-downtime deployment
|
||||
7. **Verify**: Health checks confirm success
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **`AZURE-NEW-SUBSCRIPTION-SETUP.md`** - Initial Azure setup
|
||||
- **`AZURE-APP-SERVICE-DEPLOYMENT.md`** - Manual deployment guide
|
||||
- **`AZURE-CONTAINER-REGISTRY.md`** - ACR setup
|
||||
- **`AZURE-DEVOPS-SETUP.md`** - Pipeline setup basics
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] Azure service connection created
|
||||
- [ ] Pipeline variables configured
|
||||
- [ ] Environment `production` created
|
||||
- [ ] App Services exist in Azure
|
||||
- [ ] Permissions configured
|
||||
- [ ] Pipeline tested successfully
|
||||
- [ ] Deployment verified
|
||||
- [ ] Health checks passing
|
||||
|
||||
---
|
||||
|
||||
**🎉 Your automated deployment pipeline is ready!**
|
||||
|
||||
Every push to `main` will now automatically build and deploy your application.
|
||||
152
docs/AZURE-PIPELINE-QUICK-REFERENCE.md
Normal file
152
docs/AZURE-PIPELINE-QUICK-REFERENCE.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Azure Pipeline Quick Reference
|
||||
|
||||
Quick reference for configuring and using the automated deployment pipeline.
|
||||
|
||||
## 📋 Pipeline Variables
|
||||
|
||||
Update these in `azure-pipelines.yml`:
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `acrName` | Azure Container Registry name | `cmdbinsightacr` |
|
||||
| `repositoryName` | Docker repository name | `cmdb-insight` |
|
||||
| `dockerRegistryServiceConnection` | ACR service connection name | `zuyderland-cmdb-acr-connection` |
|
||||
| `resourceGroup` | Azure resource group | `rg-cmdb-insight-prod` |
|
||||
| `backendAppName` | Backend App Service name | `cmdb-backend-prod` |
|
||||
| `frontendAppName` | Frontend App Service name | `cmdb-frontend-prod` |
|
||||
| `azureSubscription` | Azure service connection for deployment | `zuyderland-cmdb-subscription` |
|
||||
| `deployToProduction` | Enable/disable deployment | `true` or `false` |
|
||||
| `useDeploymentSlots` | Use staging slots for zero-downtime | `true` or `false` |
|
||||
|
||||
## 🔧 Required Service Connections
|
||||
|
||||
### 1. Docker Registry Connection
|
||||
|
||||
**Purpose**: Push Docker images to ACR
|
||||
|
||||
**Setup**:
|
||||
- Type: Docker Registry → Azure Container Registry
|
||||
- Name: Match `dockerRegistryServiceConnection` variable
|
||||
- Subscription: Your Azure subscription
|
||||
- Registry: Your ACR
|
||||
|
||||
### 2. Azure Resource Manager Connection
|
||||
|
||||
**Purpose**: Deploy to App Services
|
||||
|
||||
**Setup**:
|
||||
- Type: Azure Resource Manager
|
||||
- Name: Match `azureSubscription` variable
|
||||
- Subscription: Your Azure subscription
|
||||
- Authentication: Managed Identity (recommended) or Service Principal
|
||||
|
||||
## 🚀 Pipeline Stages
|
||||
|
||||
### 1. Build Stage
|
||||
- Builds backend Docker image
|
||||
- Builds frontend Docker image
|
||||
- Pushes both to ACR with tags: `$(Build.BuildId)` and `latest`
|
||||
|
||||
### 2. Deploy Stage
|
||||
- Deploys backend to App Service
|
||||
- Deploys frontend to App Service
|
||||
- Restarts both services
|
||||
- Verifies deployment
|
||||
|
||||
### 3. Verify Stage
|
||||
- Health check on backend (`/api/health`)
|
||||
- Accessibility check on frontend
|
||||
- Reports status
|
||||
|
||||
## 🎯 Triggers
|
||||
|
||||
**Automatic triggers:**
|
||||
- Push to `main` branch
|
||||
- Git tags starting with `v*` (e.g., `v1.0.0`)
|
||||
|
||||
**Manual trigger:**
|
||||
- Go to Pipelines → Your pipeline → Run pipeline
|
||||
|
||||
## 📝 Common Commands
|
||||
|
||||
### Check Pipeline Status
|
||||
```bash
|
||||
# View in Azure DevOps Portal
|
||||
# Or use Azure CLI (if configured)
|
||||
az pipelines runs list --organization <org> --project <project>
|
||||
```
|
||||
|
||||
### View Pipeline Logs
|
||||
- Go to Azure DevOps → Pipelines → Select run → View logs
|
||||
|
||||
### Cancel Running Pipeline
|
||||
- Go to Azure DevOps → Pipelines → Select run → Cancel
|
||||
|
||||
## 🔄 Deployment Flow
|
||||
|
||||
```
|
||||
Code Push → Build Images → Push to ACR → Deploy to App Services → Verify
|
||||
```
|
||||
|
||||
**With Slots:**
|
||||
```
|
||||
Code Push → Build Images → Push to ACR → Deploy to Staging → Swap to Production → Verify
|
||||
```
|
||||
|
||||
## ⚙️ Configuration Examples
|
||||
|
||||
### Basic Deployment (Current)
|
||||
```yaml
|
||||
deployToProduction: true
|
||||
useDeploymentSlots: false
|
||||
```
|
||||
→ Direct deployment to production
|
||||
|
||||
### Zero-Downtime Deployment
|
||||
```yaml
|
||||
deployToProduction: true
|
||||
useDeploymentSlots: true
|
||||
```
|
||||
→ Deploy to staging, then swap to production
|
||||
|
||||
### Build Only (No Deployment)
|
||||
```yaml
|
||||
deployToProduction: false
|
||||
```
|
||||
→ Only build and push images, don't deploy
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Pipeline Fails: "Service connection not found"
|
||||
- Check service connection name matches variable
|
||||
- Verify connection exists in Project Settings
|
||||
|
||||
### Deployment Fails: "App Service not found"
|
||||
- Verify app names match your Azure resources
|
||||
- Check resource group name is correct
|
||||
|
||||
### Images Not Updating
|
||||
- Check ACR has new images
|
||||
- Verify App Service container settings
|
||||
- Check Managed Identity has ACR pull permissions
|
||||
|
||||
## 📚 Related Files
|
||||
|
||||
- **`azure-pipelines.yml`** - Main pipeline (basic deployment)
|
||||
- **`azure-pipelines-slots.yml`** - Advanced pipeline (with slots)
|
||||
- **`docs/AZURE-PIPELINE-DEPLOYMENT.md`** - Complete setup guide
|
||||
- **`docs/AZURE-NEW-SUBSCRIPTION-SETUP.md`** - Initial Azure setup
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] Service connections created
|
||||
- [ ] Pipeline variables configured
|
||||
- [ ] Environment `production` created
|
||||
- [ ] App Services exist in Azure
|
||||
- [ ] Pipeline tested successfully
|
||||
- [ ] Deployment verified
|
||||
- [ ] Health checks passing
|
||||
|
||||
---
|
||||
|
||||
**Quick Start**: Update variables in `azure-pipelines.yml` and push to `main` branch!
|
||||
222
docs/AZURE-PIPELINES-USAGE.md
Normal file
222
docs/AZURE-PIPELINES-USAGE.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Azure Pipelines Usage Guide
|
||||
|
||||
Guide for using the separate build and deployment pipelines.
|
||||
|
||||
## 📋 Pipeline Files
|
||||
|
||||
### 1. `azure-pipelines.yml` - Build and Push Images
|
||||
|
||||
**Purpose**: Builds Docker images and pushes them to Azure Container Registry.
|
||||
|
||||
**What it does:**
|
||||
- ✅ Builds backend Docker image
|
||||
- ✅ Builds frontend Docker image
|
||||
- ✅ Pushes both to ACR with tags: `$(Build.BuildId)` and `latest`
|
||||
|
||||
**When to use:**
|
||||
- First time setup (to test image building)
|
||||
- After code changes (to build new images)
|
||||
- Before deployment (to ensure images are in ACR)
|
||||
|
||||
**Configuration:**
|
||||
```yaml
|
||||
variables:
|
||||
acrName: 'zdlas' # Your ACR name
|
||||
repositoryName: 'cmdb-insight'
|
||||
dockerRegistryServiceConnection: 'zuyderland-cmdb-acr-connection'
|
||||
```
|
||||
|
||||
### 2. `azure-pipelines-deploy.yml` - Deploy to App Service
|
||||
|
||||
**Purpose**: Deploys existing images from ACR to Azure App Services.
|
||||
|
||||
**What it does:**
|
||||
- ✅ Deploys backend container to App Service
|
||||
- ✅ Deploys frontend container to App Service
|
||||
- ✅ Restarts both App Services
|
||||
- ✅ Verifies deployment with health checks
|
||||
|
||||
**When to use:**
|
||||
- After images are built and pushed to ACR
|
||||
- When you want to deploy/update the application
|
||||
- For production deployments
|
||||
|
||||
**Configuration:**
|
||||
```yaml
|
||||
variables:
|
||||
acrName: 'zdlas' # Your ACR name
|
||||
resourceGroup: 'rg-cmdb-insight-prod' # Your resource group
|
||||
backendAppName: 'cmdb-backend-prod' # Your backend app name
|
||||
frontendAppName: 'cmdb-frontend-prod' # Your frontend app name
|
||||
azureSubscription: 'zuyderland-cmdb-subscription' # Azure service connection
|
||||
imageTag: 'latest' # Image tag to deploy
|
||||
```
|
||||
|
||||
## 🚀 Workflow
|
||||
|
||||
### Step 1: Build and Push Images
|
||||
|
||||
1. **Configure `azure-pipelines.yml`**:
|
||||
- Update `acrName` with your ACR name
|
||||
- Update `dockerRegistryServiceConnection` with your service connection name
|
||||
|
||||
2. **Create Pipeline in Azure DevOps**:
|
||||
- Go to **Pipelines** → **New pipeline**
|
||||
- Select **Existing Azure Pipelines YAML file**
|
||||
- Choose `azure-pipelines.yml`
|
||||
- Run the pipeline
|
||||
|
||||
3. **Verify Images in ACR**:
|
||||
```bash
|
||||
az acr repository list --name zdlas
|
||||
az acr repository show-tags --name zdlas --repository cmdb-insight/backend
|
||||
az acr repository show-tags --name zdlas --repository cmdb-insight/frontend
|
||||
```
|
||||
|
||||
### Step 2: Deploy Application
|
||||
|
||||
1. **Ensure App Services exist**:
|
||||
- Backend App Service: `cmdb-backend-prod`
|
||||
- Frontend App Service: `cmdb-frontend-prod`
|
||||
- See `AZURE-NEW-SUBSCRIPTION-SETUP.md` for setup instructions
|
||||
|
||||
2. **Configure `azure-pipelines-deploy.yml`**:
|
||||
- Update all variables with your Azure resource names
|
||||
- Create Azure service connection for App Service deployment
|
||||
- Create `production` environment in Azure DevOps
|
||||
|
||||
3. **Create Deployment Pipeline**:
|
||||
- Go to **Pipelines** → **New pipeline**
|
||||
- Select **Existing Azure Pipelines YAML file**
|
||||
- Choose `azure-pipelines-deploy.yml`
|
||||
- Run the pipeline
|
||||
|
||||
4. **Verify Deployment**:
|
||||
- Check backend: `https://cmdb-backend-prod.azurewebsites.net/api/health`
|
||||
- Check frontend: `https://cmdb-frontend-prod.azurewebsites.net`
|
||||
|
||||
## 🔧 Setup Requirements
|
||||
|
||||
### For Build Pipeline (`azure-pipelines.yml`)
|
||||
|
||||
**Required:**
|
||||
- ✅ Docker Registry service connection (for ACR)
|
||||
- ✅ ACR exists and is accessible
|
||||
- ✅ Service connection has push permissions
|
||||
|
||||
**Setup:**
|
||||
1. Create Docker Registry service connection:
|
||||
- **Project Settings** → **Service connections** → **New service connection**
|
||||
- Choose **Docker Registry** → **Azure Container Registry**
|
||||
- Select your ACR
|
||||
- Name: `zuyderland-cmdb-acr-connection`
|
||||
|
||||
### For Deployment Pipeline (`azure-pipelines-deploy.yml`)
|
||||
|
||||
**Required:**
|
||||
- ✅ Azure Resource Manager service connection
|
||||
- ✅ App Services exist in Azure
|
||||
- ✅ `production` environment created in Azure DevOps
|
||||
- ✅ Images exist in ACR
|
||||
|
||||
**Setup:**
|
||||
1. Create Azure service connection:
|
||||
- **Project Settings** → **Service connections** → **New service connection**
|
||||
- Choose **Azure Resource Manager**
|
||||
- Select your subscription
|
||||
- Name: `zuyderland-cmdb-subscription`
|
||||
|
||||
2. Create environment:
|
||||
- **Pipelines** → **Environments** → **Create environment**
|
||||
- Name: `production`
|
||||
- (Optional) Add approvals for manual control
|
||||
|
||||
## 📝 Typical Usage Scenarios
|
||||
|
||||
### Scenario 1: First Time Setup
|
||||
|
||||
```bash
|
||||
# 1. Build and push images
|
||||
# Run azure-pipelines.yml → Images in ACR
|
||||
|
||||
# 2. Create App Services (manual or via script)
|
||||
# See AZURE-NEW-SUBSCRIPTION-SETUP.md
|
||||
|
||||
# 3. Deploy application
|
||||
# Run azure-pipelines-deploy.yml → App deployed
|
||||
```
|
||||
|
||||
### Scenario 2: Code Update
|
||||
|
||||
```bash
|
||||
# 1. Push code to main branch
|
||||
git push origin main
|
||||
|
||||
# 2. Build pipeline runs automatically
|
||||
# azure-pipelines.yml → New images in ACR
|
||||
|
||||
# 3. Deploy new version
|
||||
# Run azure-pipelines-deploy.yml → App updated
|
||||
```
|
||||
|
||||
### Scenario 3: Deploy Specific Version
|
||||
|
||||
```bash
|
||||
# 1. Update azure-pipelines-deploy.yml
|
||||
imageTag: 'v1.0.0' # Or specific build ID
|
||||
|
||||
# 2. Run deployment pipeline
|
||||
# Deploys specific version
|
||||
```
|
||||
|
||||
## 🔄 Combining Pipelines (Future)
|
||||
|
||||
Once you're comfortable with both pipelines, you can:
|
||||
|
||||
1. **Combine them** into one pipeline with conditional deployment
|
||||
2. **Use deployment slots** for zero-downtime updates
|
||||
3. **Add approval gates** for production deployments
|
||||
|
||||
See `azure-pipelines-slots.yml` for an advanced example with deployment slots.
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Build Pipeline Fails
|
||||
|
||||
**Issue**: "Service connection not found"
|
||||
- **Solution**: Verify service connection name matches `dockerRegistryServiceConnection` variable
|
||||
|
||||
**Issue**: "ACR not found"
|
||||
- **Solution**: Check `acrName` variable matches your ACR name
|
||||
|
||||
### Deployment Pipeline Fails
|
||||
|
||||
**Issue**: "App Service not found"
|
||||
- **Solution**: Verify app names match your Azure resources
|
||||
|
||||
**Issue**: "Environment not found"
|
||||
- **Solution**: Create `production` environment in Azure DevOps
|
||||
|
||||
**Issue**: "Image not found in ACR"
|
||||
- **Solution**: Run build pipeline first to push images to ACR
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
### Build Pipeline Setup
|
||||
- [ ] Docker Registry service connection created
|
||||
- [ ] `azure-pipelines.yml` variables configured
|
||||
- [ ] Pipeline created in Azure DevOps
|
||||
- [ ] Test run successful
|
||||
- [ ] Images visible in ACR
|
||||
|
||||
### Deployment Pipeline Setup
|
||||
- [ ] Azure Resource Manager service connection created
|
||||
- [ ] `production` environment created
|
||||
- [ ] App Services exist in Azure
|
||||
- [ ] `azure-pipelines-deploy.yml` variables configured
|
||||
- [ ] Deployment pipeline created in Azure DevOps
|
||||
- [ ] Test deployment successful
|
||||
|
||||
---
|
||||
|
||||
**Workflow**: Build first → Deploy second → Verify success!
|
||||
242
docs/AZURE-RESOURCES-OVERVIEW.md
Normal file
242
docs/AZURE-RESOURCES-OVERVIEW.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Azure Resources Overview
|
||||
|
||||
Quick reference of all Azure resources needed for CMDB Insight deployment.
|
||||
|
||||
## 📋 Resources Summary
|
||||
|
||||
| Resource Type | Resource Name | Purpose | SKU/Tier | Estimated Cost | Shared? |
|
||||
|--------------|---------------|---------|----------|----------------|--------|
|
||||
| **Resource Group** | `rg-cmdb-insight-prod` | Container for all resources | - | Free | No |
|
||||
| **Container Registry** | `yourcompanyacr` | Store Docker images (can be shared) | Basic/Standard | €5-20/month | ✅ Yes |
|
||||
| **PostgreSQL Database** | `cmdb-postgres-prod` | Production database | Standard_B1ms | €20-30/month | No |
|
||||
| **Key Vault** | `kv-cmdb-insight-prod` | Store secrets securely | Standard | €1-2/month | No |
|
||||
| **App Service Plan** | `plan-cmdb-insight-prod` | Hosting plan | B1 | €15-25/month | No |
|
||||
| **App Service (Backend)** | `cmdb-backend-prod` | Backend API | - | Included in plan | No |
|
||||
| **App Service (Frontend)** | `cmdb-frontend-prod` | Frontend web app | - | Included in plan | No |
|
||||
| **Application Insights** | `appi-cmdb-insight-prod` | Monitoring & logging | Basic | €0-5/month | No |
|
||||
|
||||
**Total Estimated Cost: €41-82/month** (depending on ACR tier and usage)
|
||||
|
||||
**💡 Note**: Container Registry can be **shared across multiple applications**. The repository name (`cmdb-insight`) separates this app from others. If you already have an ACR, reuse it to save costs!
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Resource Dependencies
|
||||
|
||||
```
|
||||
Resource Group (App-specific)
|
||||
├── PostgreSQL Database
|
||||
│ └── Stores: Application data
|
||||
├── Key Vault
|
||||
│ └── Stores: Secrets (JIRA tokens, passwords, etc.)
|
||||
├── Application Insights
|
||||
│ └── Monitors: Backend & Frontend apps
|
||||
└── App Service Plan
|
||||
├── Backend App Service
|
||||
│ ├── Pulls from: Shared ACR (cmdb-insight/backend:latest)
|
||||
│ ├── Connects to: PostgreSQL
|
||||
│ ├── Reads from: Key Vault
|
||||
│ └── Sends logs to: Application Insights
|
||||
└── Frontend App Service
|
||||
├── Pulls from: Shared ACR (cmdb-insight/frontend:latest)
|
||||
└── Connects to: Backend App Service
|
||||
|
||||
Shared Resources (can be in separate resource group)
|
||||
└── Container Registry (ACR) ← Shared across multiple applications
|
||||
├── cmdb-insight/ ← This application
|
||||
│ ├── backend:latest
|
||||
│ └── frontend:latest
|
||||
├── other-app/ ← Other applications
|
||||
│ └── api:latest
|
||||
└── shared-services/ ← Shared images
|
||||
└── nginx:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Endpoints
|
||||
|
||||
After deployment, your application will be available at:
|
||||
|
||||
- **Frontend**: `https://cmdb-frontend-prod.azurewebsites.net`
|
||||
- **Backend API**: `https://cmdb-backend-prod.azurewebsites.net/api`
|
||||
- **Health Check**: `https://cmdb-backend-prod.azurewebsites.net/api/health`
|
||||
|
||||
If custom domain is configured:
|
||||
- **Frontend**: `https://cmdb.yourcompany.com`
|
||||
- **Backend API**: `https://api.cmdb.yourcompany.com` (or subdomain of your choice)
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Required Secrets
|
||||
|
||||
These secrets should be stored in Azure Key Vault:
|
||||
|
||||
| Secret Name | Description | Example |
|
||||
|-------------|-------------|---------|
|
||||
| `JiraPat` | Jira Personal Access Token (if using PAT auth) | `ATATT3xFfGF0...` |
|
||||
| `SessionSecret` | Session encryption secret | `a1b2c3d4e5f6...` (32+ chars) |
|
||||
| `JiraOAuthClientId` | Jira OAuth Client ID | `OAuthClientId123` |
|
||||
| `JiraOAuthClientSecret` | Jira OAuth Client Secret | `OAuthSecret456` |
|
||||
| `JiraSchemaId` | Jira Assets Schema ID | `schema-123` |
|
||||
| `DatabasePassword` | PostgreSQL admin password | `SecurePassword123!` |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Resource Sizing Recommendations
|
||||
|
||||
### For 20 Users (Current)
|
||||
|
||||
| Resource | Recommended SKU | Alternative |
|
||||
|----------|----------------|-------------|
|
||||
| App Service Plan | B1 (1 vCore, 1.75GB RAM) | B2 if experiencing slowness |
|
||||
| PostgreSQL | Standard_B1ms (1 vCore, 2GB RAM) | Standard_B2s for growth |
|
||||
| Container Registry | Basic (10GB) | Standard for production |
|
||||
| Key Vault | Standard | Standard (only option) |
|
||||
|
||||
### For 50+ Users (Future Growth)
|
||||
|
||||
| Resource | Recommended SKU | Notes |
|
||||
|----------|----------------|-------|
|
||||
| App Service Plan | B2 or S1 | Better performance |
|
||||
| PostgreSQL | Standard_B2s (2 vCores, 4GB RAM) | More concurrent connections |
|
||||
| Container Registry | Standard (100GB) | More storage, geo-replication |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Update/Deployment Flow
|
||||
|
||||
1. **Code Changes** → Push to repository
|
||||
2. **CI/CD Pipeline** → Builds Docker images
|
||||
3. **Push to ACR** → Images stored in Container Registry
|
||||
4. **Restart App Services** → Pulls new images from ACR
|
||||
5. **Application Updates** → New version live
|
||||
|
||||
### Manual Deployment
|
||||
|
||||
```bash
|
||||
# Restart apps to pull latest images
|
||||
az webapp restart --name cmdb-backend-prod --resource-group rg-cmdb-insight-prod
|
||||
az webapp restart --name cmdb-frontend-prod --resource-group rg-cmdb-insight-prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Security Configuration
|
||||
|
||||
### Network Security
|
||||
|
||||
- **HTTPS Only**: Enabled on both App Services
|
||||
- **Database Firewall**: Restricted to Azure services (can be further restricted)
|
||||
- **Key Vault Access**: Managed Identity only (no shared keys)
|
||||
|
||||
### Authentication
|
||||
|
||||
- **App Services**: Managed Identity for ACR and Key Vault access
|
||||
- **Database**: Username/password (stored in Key Vault)
|
||||
- **Application**: Jira OAuth 2.0 or Personal Access Token
|
||||
|
||||
---
|
||||
|
||||
## 📈 Monitoring & Logging
|
||||
|
||||
### Application Insights
|
||||
|
||||
- **Metrics**: Response times, request rates, errors
|
||||
- **Logs**: Application logs, exceptions, traces
|
||||
- **Alerts**: Configured for downtime, errors, performance issues
|
||||
|
||||
### Access Logs
|
||||
|
||||
```bash
|
||||
# Backend logs
|
||||
az webapp log tail --name cmdb-backend-prod --resource-group rg-cmdb-insight-prod
|
||||
|
||||
# Frontend logs
|
||||
az webapp log tail --name cmdb-frontend-prod --resource-group rg-cmdb-insight-prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration Files
|
||||
|
||||
### Environment Variables (Backend)
|
||||
|
||||
- `NODE_ENV=production`
|
||||
- `PORT=3001`
|
||||
- `DATABASE_TYPE=postgres`
|
||||
- `DATABASE_URL` (from Key Vault)
|
||||
- `JIRA_HOST=https://jira.zuyderland.nl`
|
||||
- `JIRA_AUTH_METHOD=oauth`
|
||||
- `JIRA_OAUTH_CLIENT_ID` (from Key Vault)
|
||||
- `JIRA_OAUTH_CLIENT_SECRET` (from Key Vault)
|
||||
- `JIRA_OAUTH_CALLBACK_URL`
|
||||
- `JIRA_SCHEMA_ID` (from Key Vault)
|
||||
- `SESSION_SECRET` (from Key Vault)
|
||||
- `FRONTEND_URL`
|
||||
- `APPINSIGHTS_INSTRUMENTATIONKEY`
|
||||
|
||||
### Environment Variables (Frontend)
|
||||
|
||||
- `VITE_API_URL` (points to backend API)
|
||||
|
||||
---
|
||||
|
||||
## 🗑️ Cleanup (If Needed)
|
||||
|
||||
To delete all resources:
|
||||
|
||||
```bash
|
||||
# Delete entire resource group (deletes all resources)
|
||||
az group delete --name rg-cmdb-insight-prod --yes --no-wait
|
||||
|
||||
# Or delete individual resources
|
||||
az acr delete --name cmdbinsightacr --resource-group rg-cmdb-insight-prod
|
||||
az postgres flexible-server delete --name cmdb-postgres-prod --resource-group rg-cmdb-insight-prod
|
||||
az keyvault delete --name kv-cmdb-insight-prod --resource-group rg-cmdb-insight-prod
|
||||
az appservice plan delete --name plan-cmdb-insight-prod --resource-group rg-cmdb-insight-prod
|
||||
```
|
||||
|
||||
**⚠️ Warning**: This will permanently delete all resources and data. Make sure you have backups if needed.
|
||||
|
||||
---
|
||||
|
||||
## 📞 Quick Commands Reference
|
||||
|
||||
```bash
|
||||
# Set variables
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
BACKEND_APP="cmdb-backend-prod"
|
||||
FRONTEND_APP="cmdb-frontend-prod"
|
||||
|
||||
# Check app status
|
||||
az webapp show --name $BACKEND_APP --resource-group $RESOURCE_GROUP --query state
|
||||
|
||||
# View logs
|
||||
az webapp log tail --name $BACKEND_APP --resource-group $RESOURCE_GROUP
|
||||
|
||||
# Restart apps
|
||||
az webapp restart --name $BACKEND_APP --resource-group $RESOURCE_GROUP
|
||||
az webapp restart --name $FRONTEND_APP --resource-group $RESOURCE_GROUP
|
||||
|
||||
# List all resources
|
||||
az resource list --resource-group $RESOURCE_GROUP --output table
|
||||
|
||||
# Get app URLs
|
||||
echo "Frontend: https://${FRONTEND_APP}.azurewebsites.net"
|
||||
echo "Backend: https://${BACKEND_APP}.azurewebsites.net/api"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **`AZURE-NEW-SUBSCRIPTION-SETUP.md`** - Complete step-by-step setup guide
|
||||
- **`AZURE-APP-SERVICE-DEPLOYMENT.md`** - Detailed App Service deployment
|
||||
- **`AZURE-CONTAINER-REGISTRY.md`** - ACR setup and usage
|
||||
- **`AZURE-QUICK-REFERENCE.md`** - Quick reference guide
|
||||
- **`PRODUCTION-DEPLOYMENT.md`** - General production deployment
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-01-21
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useEffect, useState, useCallback, useRef, Fragment } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { clsx } from 'clsx';
|
||||
import { getBIAComparison, updateApplication, getBusinessImpactAnalyses } from '../services/api';
|
||||
@@ -523,8 +523,8 @@ export default function BIASyncDashboard() {
|
||||
</tr>
|
||||
) : (
|
||||
filteredApplications.map((item) => (
|
||||
<>
|
||||
<tr key={item.id} className="hover:bg-blue-50/30 transition-colors border-b border-gray-100">
|
||||
<Fragment key={item.id}>
|
||||
<tr className="hover:bg-blue-50/30 transition-colors border-b border-gray-100">
|
||||
<td className="px-6 py-4 w-64">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
@@ -670,7 +670,7 @@ export default function BIASyncDashboard() {
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PageHeader from './PageHeader';
|
||||
|
||||
interface LifecycleApplication {
|
||||
id: string;
|
||||
|
||||
@@ -113,6 +113,7 @@ export default function TeamDashboard() {
|
||||
if (!data) return null;
|
||||
|
||||
// Sum up total FTE (including min/max bandwidth)
|
||||
// Ensure all values are numbers (handle null/undefined/NaN)
|
||||
let totalFTE = 0;
|
||||
let totalMinFTE = 0;
|
||||
let totalMaxFTE = 0;
|
||||
@@ -121,25 +122,25 @@ export default function TeamDashboard() {
|
||||
|
||||
// Aggregate from all teams
|
||||
data.teams.forEach(team => {
|
||||
totalFTE += team.totalEffort;
|
||||
totalMinFTE += team.minEffort ?? 0;
|
||||
totalMaxFTE += team.maxEffort ?? 0;
|
||||
totalApplicationCount += team.applicationCount;
|
||||
totalFTE += Number(team.totalEffort) || 0;
|
||||
totalMinFTE += Number(team.minEffort) || 0;
|
||||
totalMaxFTE += Number(team.maxEffort) || 0;
|
||||
totalApplicationCount += Number(team.applicationCount) || 0;
|
||||
|
||||
// Aggregate governance model distribution
|
||||
Object.entries(team.byGovernanceModel).forEach(([model, count]) => {
|
||||
overallByGovernanceModel[model] = (overallByGovernanceModel[model] || 0) + count;
|
||||
overallByGovernanceModel[model] = (overallByGovernanceModel[model] || 0) + (Number(count) || 0);
|
||||
});
|
||||
});
|
||||
|
||||
// Add unassigned
|
||||
totalFTE += data.unassigned.totalEffort;
|
||||
totalMinFTE += data.unassigned.minEffort ?? 0;
|
||||
totalMaxFTE += data.unassigned.maxEffort ?? 0;
|
||||
totalApplicationCount += data.unassigned.applicationCount;
|
||||
totalFTE += Number(data.unassigned.totalEffort) || 0;
|
||||
totalMinFTE += Number(data.unassigned.minEffort) || 0;
|
||||
totalMaxFTE += Number(data.unassigned.maxEffort) || 0;
|
||||
totalApplicationCount += Number(data.unassigned.applicationCount) || 0;
|
||||
|
||||
Object.entries(data.unassigned.byGovernanceModel).forEach(([model, count]) => {
|
||||
overallByGovernanceModel[model] = (overallByGovernanceModel[model] || 0) + count;
|
||||
overallByGovernanceModel[model] = (overallByGovernanceModel[model] || 0) + (Number(count) || 0);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -831,10 +832,10 @@ export default function TeamDashboard() {
|
||||
Totaal FTE
|
||||
</div>
|
||||
<div className="text-4xl font-bold text-white tracking-tight">
|
||||
{overallKPIs.totalFTE.toFixed(2)} FTE
|
||||
{(Number(overallKPIs.totalFTE) || 0).toFixed(2)} FTE
|
||||
</div>
|
||||
<div className="text-emerald-200 text-sm mt-1">
|
||||
Bandbreedte: {overallKPIs.totalMinFTE.toFixed(2)} - {overallKPIs.totalMaxFTE.toFixed(2)} FTE
|
||||
Bandbreedte: {(Number(overallKPIs.totalMinFTE) || 0).toFixed(2)} - {(Number(overallKPIs.totalMaxFTE) || 0).toFixed(2)} FTE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -847,7 +848,7 @@ export default function TeamDashboard() {
|
||||
Application Components
|
||||
</div>
|
||||
<div className="text-4xl font-bold text-white tracking-tight">
|
||||
{overallKPIs.totalApplicationCount}
|
||||
{Number(overallKPIs.totalApplicationCount) || 0}
|
||||
</div>
|
||||
<div className="text-slate-400 text-sm mt-1">
|
||||
weergegeven
|
||||
|
||||
418
scripts/setup-azure-resources.sh
Executable file
418
scripts/setup-azure-resources.sh
Executable file
@@ -0,0 +1,418 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Azure Resources Setup Script for CMDB Insight
|
||||
# This script helps automate the creation of Azure resources
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/setup-azure-resources.sh
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Azure CLI installed and logged in (az login)
|
||||
# - Appropriate permissions on Azure subscription
|
||||
# - Jira credentials ready (OAuth or PAT)
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration - Customize these values
|
||||
RESOURCE_GROUP="rg-cmdb-insight-prod"
|
||||
LOCATION="westeurope"
|
||||
|
||||
# ACR Configuration - Can be shared across multiple applications
|
||||
# Option 1: Use existing ACR (recommended if you have one)
|
||||
ACR_NAME="" # Leave empty to use existing, or set to create new
|
||||
ACR_RESOURCE_GROUP="" # Resource group where ACR exists (for existing) or will be created (for new)
|
||||
|
||||
# Option 2: Create new ACR (only if ACR_NAME is set and doesn't exist)
|
||||
# ACR_NAME="yourcompanyacr" # Must be globally unique, lowercase, 5-50 chars
|
||||
# ACR_RESOURCE_GROUP="rg-shared-services" # Or use RESOURCE_GROUP
|
||||
|
||||
DB_SERVER_NAME="cmdb-postgres-prod" # Must be globally unique
|
||||
DB_ADMIN_USER="cmdbadmin"
|
||||
DB_NAME="cmdb"
|
||||
KEY_VAULT_NAME="kv-cmdb-insight-prod" # Must be globally unique
|
||||
APP_INSIGHTS_NAME="appi-cmdb-insight-prod" # Must be globally unique
|
||||
APP_SERVICE_PLAN_NAME="plan-cmdb-insight-prod"
|
||||
BACKEND_APP_NAME="cmdb-backend-prod" # Must be globally unique
|
||||
FRONTEND_APP_NAME="cmdb-frontend-prod" # Must be globally unique
|
||||
|
||||
# SKU Configuration
|
||||
ACR_SKU="Standard" # Options: Basic, Standard, Premium (Standard recommended for shared ACR)
|
||||
APP_SERVICE_SKU="B1" # Options: B1, B2, S1
|
||||
POSTGRES_SKU="Standard_B1ms" # Options: Standard_B1ms, Standard_B2s
|
||||
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN}Azure Resources Setup for CMDB Insight${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Check Azure CLI
|
||||
if ! command -v az &> /dev/null; then
|
||||
echo -e "${RED}Error: Azure CLI is not installed.${NC}"
|
||||
echo "Install it from: https://docs.microsoft.com/cli/azure/install-azure-cli"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if logged in
|
||||
if ! az account show &> /dev/null; then
|
||||
echo -e "${YELLOW}Not logged in to Azure. Please run: az login${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show current subscription
|
||||
echo -e "${GREEN}Current Azure Subscription:${NC}"
|
||||
az account show --query "{Name:name, SubscriptionId:id}" -o table
|
||||
echo ""
|
||||
|
||||
# Confirm before proceeding
|
||||
read -p "Do you want to proceed with creating resources? (yes/no): " confirm
|
||||
if [ "$confirm" != "yes" ]; then
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Step 1: Creating Resource Group...${NC}"
|
||||
az group create \
|
||||
--name $RESOURCE_GROUP \
|
||||
--location $LOCATION \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Resource Group created${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Step 2: Setting up Azure Container Registry...${NC}"
|
||||
|
||||
# Check if ACR_NAME is provided
|
||||
if [ -z "$ACR_NAME" ]; then
|
||||
echo -e "${YELLOW}⚠️ ACR_NAME not set. Please provide an existing ACR name or set ACR_NAME to create a new one.${NC}"
|
||||
read -p "Enter existing ACR name (or press Enter to skip): " EXISTING_ACR_NAME
|
||||
if [ -z "$EXISTING_ACR_NAME" ]; then
|
||||
echo -e "${YELLOW}Skipping ACR setup. You'll need to configure ACR manually.${NC}"
|
||||
ACR_NAME=""
|
||||
ACR_LOGIN_SERVER=""
|
||||
else
|
||||
ACR_NAME=$EXISTING_ACR_NAME
|
||||
if [ -z "$ACR_RESOURCE_GROUP" ]; then
|
||||
read -p "Enter ACR resource group: " ACR_RESOURCE_GROUP
|
||||
fi
|
||||
# Verify ACR exists
|
||||
if az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP &> /dev/null; then
|
||||
echo -e "${GREEN}✓ Using existing Container Registry: $ACR_NAME${NC}"
|
||||
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP --query loginServer --output tsv)
|
||||
echo " ACR Login Server: $ACR_LOGIN_SERVER"
|
||||
else
|
||||
echo -e "${RED}Error: ACR '$ACR_NAME' not found in resource group '$ACR_RESOURCE_GROUP'${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Determine ACR resource group
|
||||
if [ -z "$ACR_RESOURCE_GROUP" ]; then
|
||||
ACR_RESOURCE_GROUP=$RESOURCE_GROUP
|
||||
fi
|
||||
|
||||
# Check if ACR already exists
|
||||
if az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP &> /dev/null; then
|
||||
echo -e "${GREEN}✓ Using existing Container Registry: $ACR_NAME${NC}"
|
||||
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP --query loginServer --output tsv)
|
||||
echo " ACR Login Server: $ACR_LOGIN_SERVER"
|
||||
else
|
||||
# Create resource group for ACR if it doesn't exist
|
||||
if ! az group show --name $ACR_RESOURCE_GROUP &> /dev/null; then
|
||||
echo "Creating resource group for ACR: $ACR_RESOURCE_GROUP"
|
||||
az group create --name $ACR_RESOURCE_GROUP --location $LOCATION --output none
|
||||
fi
|
||||
|
||||
# Create new ACR
|
||||
echo "Creating new Container Registry: $ACR_NAME"
|
||||
az acr create \
|
||||
--resource-group $ACR_RESOURCE_GROUP \
|
||||
--name $ACR_NAME \
|
||||
--sku $ACR_SKU \
|
||||
--admin-enabled true \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Container Registry created${NC}"
|
||||
|
||||
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP --query loginServer --output tsv)
|
||||
echo " ACR Login Server: $ACR_LOGIN_SERVER"
|
||||
echo -e "${YELLOW}💡 This ACR can be shared with other applications. Use repository names to separate apps.${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Step 3: Creating PostgreSQL Database...${NC}"
|
||||
echo -e "${YELLOW}Generating secure database password...${NC}"
|
||||
DB_ADMIN_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25)
|
||||
echo " Database Password: $DB_ADMIN_PASSWORD"
|
||||
echo " ${YELLOW}⚠️ Save this password securely!${NC}"
|
||||
|
||||
az postgres flexible-server create \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--name $DB_SERVER_NAME \
|
||||
--location $LOCATION \
|
||||
--admin-user $DB_ADMIN_USER \
|
||||
--admin-password $DB_ADMIN_PASSWORD \
|
||||
--sku-name $POSTGRES_SKU \
|
||||
--tier Burstable \
|
||||
--storage-size 32 \
|
||||
--version 15 \
|
||||
--public-access 0.0.0.0 \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ PostgreSQL Server created${NC}"
|
||||
|
||||
az postgres flexible-server db create \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--server-name $DB_SERVER_NAME \
|
||||
--database-name $DB_NAME \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Database created${NC}"
|
||||
|
||||
DB_HOST="${DB_SERVER_NAME}.postgres.database.azure.com"
|
||||
DB_CONNECTION_STRING="postgresql://${DB_ADMIN_USER}:${DB_ADMIN_PASSWORD}@${DB_HOST}:5432/${DB_NAME}?sslmode=require"
|
||||
echo " Database Host: $DB_HOST"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Step 4: Creating Key Vault...${NC}"
|
||||
az keyvault create \
|
||||
--name $KEY_VAULT_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--location $LOCATION \
|
||||
--sku standard \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Key Vault created${NC}"
|
||||
|
||||
# Add database password to Key Vault
|
||||
az keyvault secret set \
|
||||
--vault-name $KEY_VAULT_NAME \
|
||||
--name "DatabasePassword" \
|
||||
--value "$DB_ADMIN_PASSWORD" \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Database password stored in Key Vault${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠️ You need to add the following secrets to Key Vault manually:${NC}"
|
||||
echo " - JiraPat (if using PAT authentication)"
|
||||
echo " - SessionSecret (generate with: openssl rand -hex 32)"
|
||||
echo " - JiraOAuthClientId (if using OAuth)"
|
||||
echo " - JiraOAuthClientSecret (if using OAuth)"
|
||||
echo " - JiraSchemaId"
|
||||
echo ""
|
||||
echo "Commands to add secrets:"
|
||||
echo " az keyvault secret set --vault-name $KEY_VAULT_NAME --name SessionSecret --value \$(openssl rand -hex 32)"
|
||||
echo " az keyvault secret set --vault-name $KEY_VAULT_NAME --name JiraOAuthClientId --value <your-client-id>"
|
||||
echo " az keyvault secret set --vault-name $KEY_VAULT_NAME --name JiraOAuthClientSecret --value <your-client-secret>"
|
||||
echo " az keyvault secret set --vault-name $KEY_VAULT_NAME --name JiraSchemaId --value <your-schema-id>"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Step 5: Creating Application Insights...${NC}"
|
||||
az monitor app-insights component create \
|
||||
--app $APP_INSIGHTS_NAME \
|
||||
--location $LOCATION \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--application-type web \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Application Insights created${NC}"
|
||||
|
||||
INSTRUMENTATION_KEY=$(az monitor app-insights component show \
|
||||
--app $APP_INSIGHTS_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--query instrumentationKey --output tsv)
|
||||
echo " Instrumentation Key: $INSTRUMENTATION_KEY"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Step 6: Creating App Service Plan...${NC}"
|
||||
az appservice plan create \
|
||||
--name $APP_SERVICE_PLAN_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--sku $APP_SERVICE_SKU \
|
||||
--is-linux \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ App Service Plan created${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Step 7: Creating Backend App Service...${NC}"
|
||||
az webapp create \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--plan $APP_SERVICE_PLAN_NAME \
|
||||
--deployment-container-image-name "${ACR_NAME}.azurecr.io/cmdb-insight/backend:latest" \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Backend App Service created${NC}"
|
||||
|
||||
# Enable Managed Identity
|
||||
az webapp identity assign \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--output none
|
||||
|
||||
BACKEND_PRINCIPAL_ID=$(az webapp identity show \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--query principalId --output tsv)
|
||||
|
||||
# Grant ACR access (if ACR was configured)
|
||||
if [ -n "$ACR_NAME" ] && [ -n "$ACR_RESOURCE_GROUP" ]; then
|
||||
ACR_ID=$(az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP --query id --output tsv)
|
||||
az role assignment create \
|
||||
--assignee $BACKEND_PRINCIPAL_ID \
|
||||
--role AcrPull \
|
||||
--scope $ACR_ID \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ ACR access granted to backend${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ ACR not configured. You'll need to grant ACR access manually.${NC}"
|
||||
fi
|
||||
|
||||
# Configure container (if ACR was configured)
|
||||
if [ -n "$ACR_NAME" ]; then
|
||||
az webapp config container set \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--docker-custom-image-name "${ACR_NAME}.azurecr.io/cmdb-insight/backend:latest" \
|
||||
--docker-registry-server-url "https://${ACR_NAME}.azurecr.io" \
|
||||
--output none
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ ACR not configured. You'll need to configure container settings manually.${NC}"
|
||||
fi
|
||||
|
||||
# Grant Key Vault access
|
||||
az keyvault set-policy \
|
||||
--name $KEY_VAULT_NAME \
|
||||
--object-id $BACKEND_PRINCIPAL_ID \
|
||||
--secret-permissions get list \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Key Vault access granted to backend${NC}"
|
||||
|
||||
# Set environment variables (basic - you'll need to add Key Vault references manually)
|
||||
az webapp config appsettings set \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--settings \
|
||||
NODE_ENV=production \
|
||||
PORT=3001 \
|
||||
DATABASE_TYPE=postgres \
|
||||
DATABASE_URL="postgresql://${DB_ADMIN_USER}:${DB_ADMIN_PASSWORD}@${DB_HOST}:5432/${DB_NAME}?sslmode=require" \
|
||||
JIRA_HOST=https://jira.zuyderland.nl \
|
||||
JIRA_AUTH_METHOD=oauth \
|
||||
FRONTEND_URL="https://${FRONTEND_APP_NAME}.azurewebsites.net" \
|
||||
APPINSIGHTS_INSTRUMENTATIONKEY="${INSTRUMENTATION_KEY}" \
|
||||
--output none
|
||||
|
||||
# Enable HTTPS only
|
||||
az webapp update \
|
||||
--name $BACKEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--https-only true \
|
||||
--output none
|
||||
|
||||
echo -e "${GREEN}✓ Backend configuration complete${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Step 8: Creating Frontend App Service...${NC}"
|
||||
az webapp create \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--plan $APP_SERVICE_PLAN_NAME \
|
||||
--deployment-container-image-name "${ACR_NAME}.azurecr.io/cmdb-insight/frontend:latest" \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ Frontend App Service created${NC}"
|
||||
|
||||
# Enable Managed Identity
|
||||
az webapp identity assign \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--output none
|
||||
|
||||
FRONTEND_PRINCIPAL_ID=$(az webapp identity show \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--query principalId --output tsv)
|
||||
|
||||
# Grant ACR access (if ACR was configured)
|
||||
if [ -n "$ACR_NAME" ] && [ -n "$ACR_RESOURCE_GROUP" ]; then
|
||||
ACR_ID=$(az acr show --name $ACR_NAME --resource-group $ACR_RESOURCE_GROUP --query id --output tsv)
|
||||
az role assignment create \
|
||||
--assignee $FRONTEND_PRINCIPAL_ID \
|
||||
--role AcrPull \
|
||||
--scope $ACR_ID \
|
||||
--output none
|
||||
echo -e "${GREEN}✓ ACR access granted to frontend${NC}"
|
||||
fi
|
||||
|
||||
# Configure container (if ACR was configured)
|
||||
if [ -n "$ACR_NAME" ]; then
|
||||
az webapp config container set \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--docker-custom-image-name "${ACR_NAME}.azurecr.io/cmdb-insight/frontend:latest" \
|
||||
--docker-registry-server-url "https://${ACR_NAME}.azurecr.io" \
|
||||
--output none
|
||||
fi
|
||||
|
||||
# Set environment variables
|
||||
az webapp config appsettings set \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--settings \
|
||||
VITE_API_URL="https://${BACKEND_APP_NAME}.azurewebsites.net/api" \
|
||||
--output none
|
||||
|
||||
# Enable HTTPS only
|
||||
az webapp update \
|
||||
--name $FRONTEND_APP_NAME \
|
||||
--resource-group $RESOURCE_GROUP \
|
||||
--https-only true \
|
||||
--output none
|
||||
|
||||
echo -e "${GREEN}✓ Frontend configuration complete${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN}Setup Complete!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next Steps:${NC}"
|
||||
echo ""
|
||||
echo "1. Add secrets to Key Vault:"
|
||||
echo " az keyvault secret set --vault-name $KEY_VAULT_NAME --name SessionSecret --value \$(openssl rand -hex 32)"
|
||||
echo " az keyvault secret set --vault-name $KEY_VAULT_NAME --name JiraOAuthClientId --value <your-client-id>"
|
||||
echo " az keyvault secret set --vault-name $KEY_VAULT_NAME --name JiraOAuthClientSecret --value <your-client-secret>"
|
||||
echo " az keyvault secret set --vault-name $KEY_VAULT_NAME --name JiraSchemaId --value <your-schema-id>"
|
||||
echo ""
|
||||
echo "2. Update backend app settings to use Key Vault references:"
|
||||
echo " See AZURE-NEW-SUBSCRIPTION-SETUP.md for details"
|
||||
echo ""
|
||||
echo "3. Build and push Docker images:"
|
||||
if [ -n "$ACR_NAME" ]; then
|
||||
echo " export ACR_NAME=\"$ACR_NAME\""
|
||||
echo " ./scripts/build-and-push-azure.sh"
|
||||
else
|
||||
echo " export ACR_NAME=\"<your-acr-name>\""
|
||||
echo " ./scripts/build-and-push-azure.sh"
|
||||
fi
|
||||
echo ""
|
||||
echo "4. Restart apps to pull images:"
|
||||
echo " az webapp restart --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP"
|
||||
echo " az webapp restart --name $FRONTEND_APP_NAME --resource-group $RESOURCE_GROUP"
|
||||
echo ""
|
||||
echo -e "${GREEN}Resource Information:${NC}"
|
||||
echo " Resource Group: $RESOURCE_GROUP"
|
||||
if [ -n "$ACR_LOGIN_SERVER" ]; then
|
||||
echo " ACR: $ACR_LOGIN_SERVER"
|
||||
echo " Repository: cmdb-insight (backend, frontend)"
|
||||
else
|
||||
echo " ACR: Not configured (configure manually)"
|
||||
fi
|
||||
echo " Database: $DB_HOST"
|
||||
echo " Key Vault: $KEY_VAULT_NAME"
|
||||
echo " Frontend URL: https://${FRONTEND_APP_NAME}.azurewebsites.net"
|
||||
echo " Backend URL: https://${BACKEND_APP_NAME}.azurewebsites.net/api"
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠️ Important: Save the database password securely!${NC}"
|
||||
echo " Password: $DB_ADMIN_PASSWORD"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user