Migrated Function deployment to v2 model, updated README, modified health check url and moved tools to properly named directory

This commit is contained in:
Matthew Garrett 2024-02-03 19:45:52 -08:00
Родитель d2ccb87487
Коммит 15b8120952
16 изменённых файлов: 211 добавлений и 174 удалений

Просмотреть файл

@ -32,20 +32,23 @@ Azure IPAM is a lightweight solution developed on top of the Azure platform desi
| `.github/` | Bug Report, Issue Templates and GitHub Actions |
| `.vscode/` | VSCode Configuration |
| `deploy/` | Deployment Bicep Templates & PowerShell Deployment Scripts |
| `assets/` | Compiled ZIP Archive |
| `docs/` | Documentation Folder |
| `engine/` | Engine Application Code |
| `examples/` | Example Templates, Scripts and Code Snippets for Azure IPAM |
| `lb/` | Load Balancer (NGINX) Configs |
| `tests/` | Engine and UI Testing Scripts |
| `tests/` | Testing Scripts |
| `tools/` | Lifecycle Scripts (Build/Version/Update) |
| `ui/` | UI Application Code |
| `.dockerignore` | Untracked Docker Files to Ignore |
| `.env.example` | Example ENV File to be Used with Docker Compose |
| `.gitattributes` | Git File and Path Attributes |
| `.gitignore` | Untracked Git Files to Ignore |
| `CODE_OF_CONDUCT.md` | Microsoft Code of Conduct |
| `docker-compose.prod.yml` | Production Docker Compose File |
| `docker-compose.yml` | Development Docker Compose File |
| `Dockerfile` | Single Container Dockerfile |
| `Dockerfile.deb` | Single Container Dockerfile (Debian) |
| `Dockerfile.func` | Single Container Dockerfile (Function) |
| `Dockerfile.rhel` | Single Container Dockerfile (Red Hat) |
| `init.sh` | Single Container Init Script |
| `LICENSE` | Microsoft MIT License |
| `README.md` | This README File |

Двоичные данные
assets/ipam.zip

Двоичный файл не отображается.

Просмотреть файл

@ -78,7 +78,7 @@ resource appService 'Microsoft.Web/sites@2021-02-01' = {
alwaysOn: true
linuxFxVersion: deployAsContainer ? 'DOCKER|${acrUri}/ipam:latest' : 'PYTHON|3.9'
appCommandLine: !deployAsContainer ? 'init.sh 8000' : null
healthCheckPath: '/api/docs'
healthCheckPath: '/api/status'
appSettings: concat(
[
{

Просмотреть файл

@ -253,7 +253,8 @@ DynamicParam {
if ($invalidFields -or $missingFields) {
$deploymentType = $PrivateAcr ? "'$($PSCmdlet.ParameterSetName) w/ Private ACR'" : $PSCmdlet.ParameterSetName
Write-Host "ERROR: Missing or improperly formatted field(s) in 'ResourceNames' parameter for deploment type '$deploymentType'" -ForegroundColor Red
Write-Host
Write-Host "ERROR: Missing or improperly formatted field(s) in 'ResourceNames' parameter for deploment type $deploymentType" -ForegroundColor Red
foreach ($field in $invalidFields) {
Write-Host "ERROR: Invalid Field ->" $field -ForegroundColor Red
@ -266,7 +267,7 @@ DynamicParam {
Write-Host "ERROR: Please refer to the 'Naming Rules and Restrictions for Azure Resources'" -ForegroundColor Red
Write-Host "ERROR: " -ForegroundColor Red -NoNewline
Write-Host "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules" -ForegroundColor Yellow
Write-Host ""
Write-Host
throw [System.ArgumentException]::New("One of the required resource names is missing or invalid.")
}
@ -294,7 +295,7 @@ process {
$AZURE_ENV_MAP = @{
AzureCloud = "AZURE_PUBLIC"
AzureUSGovernment = "AZURE_US_GOV"
AzureUSSecret = "AZURE_US_GOV_SECRET"
USSec = "AZURE_US_GOV_SECRET"
AzureGermanCloud = "AZURE_GERMANY"
AzureChinaCloud = "AZURE_CHINA"
}
@ -613,7 +614,8 @@ process {
$accesstoken = (Get-AzAccessToken -Resource "https://$($msGraphMap[$AzureCloud].Endpoint)/").Token
# Switch Access Token to SecureString if Graph Version is 2.x
$graphVersion = [System.Version](Get-InstalledModule -Name Microsoft.Graph).Version
$graphVersion = [System.Version](Get-InstalledModule -Name Microsoft.Graph | Sort-Object -Property Version | Select-Object -Last 1).Version `
?? (Get-Module -Name Microsoft.Graph | Sort-Object -Property Version | Select-Object -Last 1).Version
if ($graphVersion.Major -gt 1) {
$accesstoken = ConvertTo-SecureString $accesstoken -AsPlainText -Force
@ -842,6 +844,72 @@ process {
return $deployment
}
Function Publish-ZipFile {
Param(
[Parameter(Mandatory=$true)]
[string]$AppName,
[Parameter(Mandatory=$true)]
[string]$ResourceGroupName,
[Parameter(Mandatory=$false)]
[switch]$UseAPI
)
if ($UseAPI) {
Write-Host "INFO: Using Kudu API for ZIP Deploy" -ForegroundColor Green
}
$zipPath = Join-Path -Path $ROOT_DIR -ChildPath 'assets' -AdditionalChildPath "ipam.zip"
$publishRetries = 3
$publishSuccess = $False
if ($UseAPI) {
$accessToken = (Get-AzAccessToken).Token
$zipContents = Get-Item -Path $zipPath
$publishProfile = Get-AzWebAppPublishingProfile -Name $AppName -ResourceGroupName $ResourceGroupName
$zipUrl = ([System.uri]($publishProfile | Select-Xml -XPath "//publishProfile[@publishMethod='ZipDeploy']" | Select-Object -ExpandProperty Node).publishUrl).Scheme
}
do {
try {
if (-not $UseAPI) {
Publish-AzWebApp `
-Name $AppName `
-ResourceGroupName $ResourceGroupName `
-ArchivePath $zipPath `
-Restart `
-Force `
| Out-Null
} else {
Invoke-RestMethod `
-Uri "https://${zipUrl}/api/zipdeploy" `
-Method Post `
-ContentType "multipart/form-data" `
-Headers @{ "Authorization" = "Bearer $accessToken" } `
-Form @{ file = $zipContents } `
-StatusCodeVariable statusCode `
| Out-Null
if ($statusCode -ne 200) {
throw [System.Exception]::New("Error while uploading ZIP Deploy via Kudu API! ($statusCode)")
}
}
$publishSuccess = $True
Write-Host "INFO: ZIP Deploy archive successfully uploaded" -ForegroundColor Green
} catch {
if($publishRetries -gt 0) {
Write-Host "WARNING: Problem while uploading ZIP Deploy archive! Retrying..." -ForegroundColor Yellow
$publishRetries--
} else {
Write-Host "ERROR: Unable to upload ZIP Deploy archive!" -ForegroundColor Red
throw $_
}
}
} while ($publishSuccess -eq $False -and $publishRetries -ge 0)
}
Function Update-UIApplication {
Param(
[Parameter(Mandatory=$true)]
@ -983,26 +1051,12 @@ process {
if ($PSCmdlet.ParameterSetName -in ('App', 'Function')) {
Write-Host "INFO: Uploading ZIP Deploy archive..." -ForegroundColor Green
$zipPath = Join-Path -Path $ROOT_DIR -ChildPath 'assets' -AdditionalChildPath "ipam.zip"
$publishRetries = 5
$publishSuccess = $False
do {
try {
Publish-AzWebApp -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value -Name $deployment.Outputs["appServiceName"].Value -ArchivePath $zipPath -Restart -Force | Out-Null
$publishSuccess = $True
Write-Host "INFO: ZIP Deploy archive successfully uploaded" -ForegroundColor Green
} catch {
if($publishRetries -gt 0) {
Write-Host "WARNING: Problem while uploading ZIP Deploy archive! Retrying..." -ForegroundColor Yellow
$publishRetries--
} else {
Write-Host "ERROR: Unable to upload ZIP Deploy archive!" -ForegroundColor Red
throw $_
}
}
} while ($publishSuccess -eq $False -and $publishRetries -gt 0)
try {
Publish-ZipFile -AppName $deployment.Outputs["appServiceName"].Value -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value
} catch {
Write-Host "SWITCH: Retrying ZIP Deploy with Kudu API..." -ForegroundColor Blue
Publish-ZipFile -AppName $deployment.Outputs["appServiceName"].Value -ResourceGroupName $deployment.Outputs["resourceGroupName"].Value -UseAPI
}
}
if ($PSCmdlet.ParameterSetName -in ('AppContainer', 'FunctionContainer') -and $PrivateAcr) {

Просмотреть файл

@ -81,7 +81,7 @@ resource functionApp 'Microsoft.Web/sites@2021-03-01' = {
acrUseManagedIdentityCreds: privateAcr ? true : false
acrUserManagedIdentityID: privateAcr ? managedIdentityClientId : null
linuxFxVersion: deployAsContainer ? 'DOCKER|${acrUri}/ipamfunc:latest' : 'Python|3.9'
healthCheckPath: '/api/docs'
healthCheckPath: '/api/status'
appSettings: concat(
[
{

Просмотреть файл

@ -14,6 +14,8 @@ from app.routers.common.helper import (
from app.globals import globals
from app.logs.logs import ipam_logger as logger
_session = None
async def fetch_jwks_keys():
@ -76,6 +78,16 @@ async def validate_token(request: Request):
except Exception:
raise HTTPException(status_code=401, detail="Unable to parse authorization token.")
try:
token_version = int(jwt.decode(token, options={"verify_signature": False})["ver"].split(".")[0])
except Exception:
raise HTTPException(status_code=401, detail="Unable to decode token version.")
if token_version == 1:
logger.error("Microsoft Identity v1.0 access tokens are not supported!")
logger.error("https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens#token-formats")
raise HTTPException(status_code=401, detail="Microsoft Identity v1.0 access tokens are not supported.")
if rsa_key:
rsa_pem_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(rsa_key))
rsa_pem_key_bytes = rsa_pem_key.public_bytes(
@ -99,10 +111,10 @@ async def validate_token(request: Request):
except jwt.InvalidSignatureError:
raise HTTPException(status_code=401, detail="Invalid token signature.")
except Exception:
raise HTTPException(status_code=401, detail="Unable to parse authorization token.")
raise HTTPException(status_code=401, detail="Unable to decode authorization token.")
else:
raise HTTPException(status_code=401, detail="Unable to find appropriate signing key.")
request.state.tenant_id = payload['tid']
return payload

Просмотреть файл

@ -470,9 +470,8 @@ async def ipam_startup():
await db_upgrade()
# https://github.com/yuval9313/FastApi-RESTful/issues/138
@app.on_event("startup")
@repeat_every(seconds = 60, wait_first = True) # , wait_first=True
@repeat_every(seconds = 60, wait_first = True)
async def find_reservations() -> None:
if not os.environ.get("FUNCTIONS_WORKER_RUNTIME"):
try:
@ -480,7 +479,7 @@ async def find_reservations() -> None:
except Exception as e:
logger.error('Error running network check loop!')
tb = traceback.format_exc()
logger.debug(tb);
logger.debug(tb)
raise e
@app.exception_handler(StarletteHTTPException)

33
engine/function_app.py Normal file
Просмотреть файл

@ -0,0 +1,33 @@
import logging
import traceback
from datetime import datetime, timezone
import azure.functions as func
from app.main import app as ipam
from app.logs.logs import ipam_logger as logger
from app.routers.azure import match_resv_to_vnets
azureLogger = logging.getLogger('azure')
azureLogger.setLevel(logging.ERROR)
app = func.AsgiFunctionApp(app=ipam, http_auth_level=func.AuthLevel.ANONYMOUS)
# @app.function_name(name="ipam-sentinel")
# @app.schedule(schedule="0 * * * * *", arg_name="mytimer", run_on_startup=True, use_monitor=False)
@app.timer_trigger(schedule="0 * * * * *", arg_name="mytimer", run_on_startup=True, use_monitor=False)
async def ipam_sentinel(mytimer: func.TimerRequest) -> None:
utc_timestamp = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat()
logger.info('Azure IPAM Sentinel function was triggered')
if mytimer.past_due:
logger.debug('The timer is past due ({})!'.format(utc_timestamp))
try:
await match_resv_to_vnets()
except Exception as e:
logger.error('Error running network check loop!')
tb = traceback.format_exc()
logger.debug(tb)
raise e

Просмотреть файл

@ -6,6 +6,9 @@
}
},
"logging": {
"logLevel": {
"default": "Information"
},
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,

Просмотреть файл

@ -1,58 +0,0 @@
import azure.functions as func
from azure.functions._http_asgi import AsgiResponse, AsgiRequest
# https://github.com/Azure-Samples/fastapi-on-azure-functions/issues/4
# https://github.com/Azure/azure-functions-python-library/pull/143
# import nest_asyncio
import sys
import logging
from app.main import app as ipam
# https://github.com/Azure-Samples/fastapi-on-azure-functions/issues/4
# https://github.com/Azure/azure-functions-python-library/pull/143
# nest_asyncio.apply()
logger = logging.getLogger('azure')
logger.setLevel(logging.ERROR)
IS_INITED = False
async def run_setup(app):
"""Workaround to run Starlette startup events on Azure Function Workers."""
global IS_INITED
if not IS_INITED:
await app.router.startup()
IS_INITED = True
async def handle_asgi_request(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
asgi_request = AsgiRequest(req, context)
scope = asgi_request.to_asgi_http_scope()
asgi_response = await AsgiResponse.from_app(ipam, scope, req.get_body())
return asgi_response.to_func_response()
async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
await run_setup(ipam)
return await handle_asgi_request(req, context)
# Current issue where FastAPI startup is ignored:
# https://github.com/Azure/azure-functions-python-worker/issues/911
#
# Target function format after fix:
#
# async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
# """Each request is redirected to the ASGI handler."""
# return await func.AsgiMiddleware(ipam).handle_async(req, context)
# Keeping an eye on this fix as well:
# https://github.com/Azure/azure-functions-python-library/pull/148
# Log Stream Flood Issue(s)
# https://github.com/Azure/azure-functions-dotnet-worker/issues/796
# https://github.com/Azure/azure-functions-host/issues/8973
# See logging.getLogger('azure') workaround above...

Просмотреть файл

@ -1,24 +0,0 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post",
"patch",
"put",
"delete"
],
"route": "/{*route}"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}

Просмотреть файл

@ -1,18 +0,0 @@
import datetime
import azure.functions as func
from app.logs.logs import ipam_logger as logger
from app.routers.azure import match_resv_to_vnets
async def main(mytimer: func.TimerRequest) -> None:
utc_timestamp = datetime.datetime.utcnow().replace(
tzinfo=datetime.timezone.utc).isoformat()
if mytimer.past_due:
logger.info('The timer is past due!')
logger.info('Python timer trigger function ran at %s', utc_timestamp)
await match_resv_to_vnets()

Просмотреть файл

@ -1,11 +0,0 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "mytimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "0 * * * * *"
}
]
}

Просмотреть файл

@ -114,8 +114,7 @@ try {
$FilePath = Join-Path -Path $Path -ChildPath $FileName
Compress-Archive -Path ..\engine\app -DestinationPath $FilePath -Force
Compress-Archive -Path ..\engine\ipam-func -DestinationPath $FilePath -Update
Compress-Archive -Path ..\engine\ipam-sentinel -DestinationPath $FilePath -Update
Compress-Archive -Path ..\engine\function_app.py -DestinationPath $FilePath -Update
Compress-Archive -Path ..\engine\requirements.txt -DestinationPath $FilePath -Update
Compress-Archive -Path ..\engine\host.json -DestinationPath $FilePath -Update
Compress-Archive -Path ..\ui\dist -DestinationPath $FilePath -Update

Просмотреть файл

@ -89,6 +89,72 @@ Function Restart-IpamApp {
} while ($restartSuccess -eq $False -and $restartRetries -gt 0)
}
Function Publish-ZipFile {
Param(
[Parameter(Mandatory=$true)]
[string]$AppName,
[Parameter(Mandatory=$true)]
[string]$ResourceGroupName,
[Parameter(Mandatory=$false)]
[switch]$UseAPI
)
if ($UseAPI) {
Write-Host "INFO: Using Kudu API for ZIP Deploy" -ForegroundColor Green
}
$zipPath = Join-Path -Path $ROOT_DIR -ChildPath 'assets' -AdditionalChildPath "ipam.zip"
$publishRetries = 3
$publishSuccess = $False
if ($UseAPI) {
$accessToken = (Get-AzAccessToken).Token
$zipContents = Get-Item -Path $zipPath
$publishProfile = Get-AzWebAppPublishingProfile -Name $AppName -ResourceGroupName $ResourceGroupName
$zipUrl = ([System.uri]($publishProfile | Select-Xml -XPath "//publishProfile[@publishMethod='ZipDeploy']" | Select-Object -ExpandProperty Node).publishUrl).Scheme
}
do {
try {
if (-not $UseAPI) {
Publish-AzWebApp `
-Name $AppName `
-ResourceGroupName $ResourceGroupName `
-ArchivePath $zipPath `
-Restart `
-Force `
| Out-Null
} else {
Invoke-RestMethod `
-Uri "https://${zipUrl}/api/zipdeploy" `
-Method Post `
-ContentType "multipart/form-data" `
-Headers @{ "Authorization" = "Bearer $accessToken" } `
-Form @{ file = $zipContents } `
-StatusCodeVariable statusCode `
| Out-Null
if ($statusCode -ne 200) {
throw [System.Exception]::New("Error while uploading ZIP Deploy via Kudu API! ($statusCode)")
}
}
$publishSuccess = $True
Write-Host "INFO: ZIP Deploy archive successfully uploaded" -ForegroundColor Green
} catch {
if($publishRetries -gt 0) {
Write-Host "WARNING: Problem while uploading ZIP Deploy archive! Retrying..." -ForegroundColor Yellow
$publishRetries--
} else {
Write-Host "ERROR: Unable to upload ZIP Deploy archive!" -ForegroundColor Red
throw $_
}
}
} while ($publishSuccess -eq $False -and $publishRetries -ge 0)
}
Start-Transcript -Path $updateLog | Out-Null
try {
@ -267,33 +333,12 @@ try {
} else {
Write-Host "INFO: Uploading ZIP Deploy archive..." -ForegroundColor Green
$zipPath = Join-Path -Path $ROOT_DIR -ChildPath 'assets' -AdditionalChildPath "ipam.zip"
$publishRetries = 5
$publishSuccess = $False
do {
try {
Publish-AzWebApp `
-Name $AppName `
-ResourceGroupName $ResourceGroupName `
-ArchivePath $zipPath `
-Restart `
-Force `
| Out-Null
$publishSuccess = $True
Write-Host "INFO: ZIP Deploy archive successfully uploaded" -ForegroundColor Green
} catch {
if($publishRetries -gt 0) {
Write-Host "WARNING: Problem while uploading ZIP Deploy archive! Retrying..." -ForegroundColor Yellow
$publishRetries--
} else {
Write-Host "ERROR: Unable to upload ZIP Deploy archive!" -ForegroundColor Red
throw $_
}
}
} while ($publishSuccess -eq $False -and $publishRetries -gt 0)
try {
Publish-ZipFile -AppName $AppName -ResourceGroupName $ResourceGroupName
} catch {
Write-Host "SWITCH: Retrying ZIP Deploy with Kudu API..." -ForegroundColor Blue
Publish-ZipFile -AppName $AppName -ResourceGroupName $ResourceGroupName -UseAPI
}
Write-Host
Write-Host "NOTE: Please allow ~5 minutes for the ZIP Deploy process to complete" -ForegroundColor Yellow

Просмотреть файл