diff --git a/.github/workflows/_functionDeployTemplate.yml b/.github/workflows/_functionDeployTemplate.yml new file mode 100644 index 0000000..91a47bc --- /dev/null +++ b/.github/workflows/_functionDeployTemplate.yml @@ -0,0 +1,97 @@ +name: Function App Deploy Template + +on: + workflow_call: + inputs: + environment: + required: true + type: string + default: "dev" + description: "Specifies the environment of the deployment." + python_version: + required: true + type: string + default: "3.11" + description: "Specifies the python version." + function_directory: + required: true + type: string + description: "Specifies the directory of the Azure Web App." + function_name: + required: true + type: string + description: "Specifies the name of the Azure Web App." + tenant_id: + required: true + type: string + description: "Specifies the tenant id of the deployment." + subscription_id: + required: true + type: string + description: "Specifies the subscription id of the deployment." + secrets: + CLIENT_ID: + required: true + description: "Specifies the client id." + +permissions: + id-token: write + contents: read + +jobs: + deployment: + name: Function App Deploy + runs-on: [ubuntu-latest] + continue-on-error: false + environment: ${{ inputs.environment }} + concurrency: + group: function-${{ inputs.function_name }}-${{ inputs.environment }} + cancel-in-progress: false + + steps: + # Check Out Repository + - name: Check Out Repository + id: checkout_repository + uses: actions/checkout@v4 + + # Setup Python + - name: Setup Python + id: python_setup + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + cache: "pip" + cache-dependency-path: | + ${{ inputs.function_directory }}/requirements.txt + + # Install Web App Dependencies + - name: Resolve Web App Dependencies + id: function_dependencies + shell: bash + run: | + pushd "${WEBAPP_DIRECTORY}" + python -m pip install --upgrade pip + pip install -r requirements.txt --target=".python_packages/lib/site-packages" + popd + env: + WEBAPP_DIRECTORY: ${{ inputs.function_directory }} + + # Azure login + - name: Azure login + id: azure_login + uses: azure/login@v2 + with: + client-id: ${{ secrets.CLIENT_ID }} + tenant-id: ${{ inputs.tenant_id }} + subscription-id: ${{ inputs.subscription_id }} + + # Deploy Function + - name: Deploy Function + id: function_deploy + uses: Azure/functions-action@v1 + with: + app-name: ${{ inputs.function_name }} + package: ${{ inputs.function_directory }} + sku: flexconsumption + remote-build: true + respect-funcignore: true diff --git a/.github/workflows/function.yml b/.github/workflows/function.yml new file mode 100644 index 0000000..3863186 --- /dev/null +++ b/.github/workflows/function.yml @@ -0,0 +1,39 @@ +name: Function Deployment +on: + push: + branches: + - main + paths: + - "code/function/**" + - "config/**" + - "requirements.txt" + + pull_request: + branches: + - main + paths: + - "code/function/**" + - "config/**" + - "requirements.txt" + +jobs: + function_test: + uses: ./.github/workflows/_webappTestTemplate.yml + name: "Function Test" + with: + python_version: "3.11" + webapp_directory: "./code/function" + + function_dev: + uses: ./.github/workflows/_functionDeployTemplate.yml + name: "Function - Dev" + needs: [function_test] + with: + environment: "dev" + python_version: "3.11" + function_directory: "./code/function" + function_name: "bfr-dev-fctn001" + tenant_id: "37963dd4-f4e6-40f8-a7d6-24b97919e452" + subscription_id: "1fdab118-1638-419a-8b12-06c9543714a0" + secrets: + CLIENT_ID: ${{ secrets.CLIENT_ID }} diff --git a/.github/workflows/webapp.yml b/.github/workflows/webapp.yml index c3a6115..704cc3b 100644 --- a/.github/workflows/webapp.yml +++ b/.github/workflows/webapp.yml @@ -4,20 +4,16 @@ on: branches: - main paths: - - "**.py" - "code/backend/**" - "config/**" - - "tests/**" - "requirements.txt" pull_request: branches: - main paths: - - "**.py" - "code/backend/**" - "config/**" - - "tests/**" - "requirements.txt" jobs: diff --git a/code/function/.funcignore b/code/function/.funcignore new file mode 100644 index 0000000..43f8868 --- /dev/null +++ b/code/function/.funcignore @@ -0,0 +1,10 @@ +.git* +.vscode +__azurite_db*__.json +__blobstorage__ +__queuestorage__ +__pycache__ +local.settings.json +tests +.venv +.vscode diff --git a/code/function/blobfiletrigger/__init__.py b/code/function/blobfiletrigger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/function/blobfiletrigger/function.py b/code/function/blobfiletrigger/function.py new file mode 100644 index 0000000..80090d3 --- /dev/null +++ b/code/function/blobfiletrigger/function.py @@ -0,0 +1,29 @@ +import logging + +import azure.functions as func +import azurefunctions.extensions.bindings.blob as blob +from shared.config import settings +from shared.utils import get_blob_properties + +bp = func.Blueprint() + + +@bp.function_name("VideoUpload") +@bp.blob_trigger( + arg_name="client", + path="upload-newsvideos/{name}", + connection="BlobTrigger", + # source="LogsAndContainerScan", +) +async def upload_video(client: blob.BlobClient): + logging.info("File upload detected.") + + # Initialize + logging.info("Initialize") + _ = await get_blob_properties( + storage_domain_name=f"{client.account_name}.blob.core.windows.net", + storage_container_name=client.container_name, + storage_blob_name=client.blob_name, + managed_identity_client_id=settings.MANAGED_IDENTITY_CLIENT_ID, + ) + logging.info(f"Completed Function run successfully.") diff --git a/code/function/function_app.py b/code/function/function_app.py new file mode 100644 index 0000000..76de8d6 --- /dev/null +++ b/code/function/function_app.py @@ -0,0 +1,10 @@ +import azure.functions as func + +# from blobfiletrigger.function import bp as bp_blobfiletrigger +from health.function import bp as bp_health + +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) + +# Register blueprints +app.register_functions(bp_health) +# app.register_functions(bp_blobfiletrigger) diff --git a/code/function/health/__init__.py b/code/function/health/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/function/health/function.py b/code/function/health/function.py new file mode 100644 index 0000000..bcbc656 --- /dev/null +++ b/code/function/health/function.py @@ -0,0 +1,17 @@ +import logging + +import azure.functions as func +from health.models import HeartbeatResponse + +bp = func.Blueprint() + + +@bp.function_name("Health") +@bp.route(route="v1/heartbeat") +@bp.http_type(http_type=func.HttpMethod.GET) +async def heartbeat(req: func.HttpRequest) -> func.HttpResponse: + logging.debug("Received Health Request.") + response = HeartbeatResponse(is_alive=True).model_dump_json() + return func.HttpResponse( + body=response, status_code=200, mimetype="application/json" + ) diff --git a/code/function/health/models.py b/code/function/health/models.py new file mode 100644 index 0000000..a02c45c --- /dev/null +++ b/code/function/health/models.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class HeartbeatResponse(BaseModel): + is_alive: bool = True diff --git a/code/function/host.json b/code/function/host.json new file mode 100644 index 0000000..afc54ab --- /dev/null +++ b/code/function/host.json @@ -0,0 +1,42 @@ +{ + "version": "2.0", + "functionTimeout": "00:30:00", + "logging": { + "fileLoggingMode": "debugOnly", + "logLevel": { + "default": "Information", + "Worker": "Information", + "Host": "Information", + "Host.Aggregator": "Information", + "Host.Results": "Information", + "Function": "Information" + }, + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request;Exception" + }, + "enableLiveMetrics": true, + "enableDependencyTracking": true, + "enablePerformanceCountersCollection": true, + "httpAutoCollectionOptions": { + "enableHttpTriggerExtendedInfoCollection": true, + "enableW3CDistributedTracing": true, + "enableResponseHeaderInjection": true + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + }, + "extensions": { + "http": { + "routePrefix": "api" + }, + "blobs": { + "maxDegreeOfParallelism": 4, + "poisonBlobThreshold": 1 + } + } +} diff --git a/code/function/requirements.txt b/code/function/requirements.txt new file mode 100644 index 0000000..4f231ff --- /dev/null +++ b/code/function/requirements.txt @@ -0,0 +1,9 @@ +azure-functions~=1.21.3 +azurefunctions-extensions-bindings-blob==1.0.0b2 +azure-identity~=1.19.0 +aiohttp~=3.10.5 +httpx~=0.27.2 +pydantic-settings~=2.6.1 +pydantic~=2.9.2 +langchain~=0.3.7 +langchain-openai~=0.2.5 diff --git a/code/function/shared/__init__.py b/code/function/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/function/shared/config.py b/code/function/shared/config.py new file mode 100644 index 0000000..5667c44 --- /dev/null +++ b/code/function/shared/config.py @@ -0,0 +1,27 @@ +import logging + +from pydantic import Field +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + # General config + PROJECT_NAME: str = "VideoAnalyzer" + SERVER_NAME: str = "VideoAnalyzer" + APP_VERSION: str = "v0.0.1" + WEBSITE_NAME: str = Field(default="test", alias="WEBSITE_SITE_NAME") + WEBSITE_INSTANCE_ID: str = Field(default="0", alias="WEBSITE_INSTANCE_ID") + HOME_DIRECTORY: str = Field(default="", alias="HOME") + + # Identity settings + MANAGED_IDENTITY_CLIENT_ID: str + + # Logging settings + LOGGING_LEVEL: int = logging.INFO + DEBUG: bool = False + APPLICATIONINSIGHTS_CONNECTION_STRING: str = Field( + default="", alias="APPLICATIONINSIGHTS_CONNECTION_STRING" + ) + + +settings = Settings() diff --git a/code/function/shared/models.py b/code/function/shared/models.py new file mode 100644 index 0000000..e615c5a --- /dev/null +++ b/code/function/shared/models.py @@ -0,0 +1,9 @@ +from typing import Any + +from pydantic import BaseModel + + +class ErrorModel(BaseModel): + error_code: int + error_message: str + error_details: Any diff --git a/code/function/shared/utils.py b/code/function/shared/utils.py new file mode 100644 index 0000000..51db60a --- /dev/null +++ b/code/function/shared/utils.py @@ -0,0 +1,58 @@ +import logging + +from azure.identity.aio import DefaultAzureCredential +from azure.storage.blob import BlobProperties +from azure.storage.blob.aio import BlobServiceClient + + +def get_azure_credential( + managed_identity_client_id: str = None, +) -> DefaultAzureCredential: + """Creates a default azure crendetial used for authentication. + + managed_identity_client_id (str): Specifies the client id of a managed identity. + RETURNS (str): Returns a default azure credential. + """ + if managed_identity_client_id is None: + return DefaultAzureCredential() + else: + return DefaultAzureCredential( + managed_identity_client_id=managed_identity_client_id, + ) + + +async def get_blob_properties( + storage_domain_name: str, + storage_container_name: str, + storage_blob_name: str, + managed_identity_client_id: str = None, +) -> BlobProperties: + """Copy file from source blob storage container async to sink blob storage container. + + storage_domain_name (str): The domain name of the storage account to which the file will be copied. + storage_container_name (str): The container name of the storage account. + storage_blob_name (str): The blob name of the storage account. + managed_identity_client_id (str): Specifies the managed identity client id used for auth. + RETURNS (BlobProperties): Returns the properties of the blob. + """ + logging.info( + f"Get properties for blob: 'https://{storage_domain_name}/{storage_container_name}/{storage_blob_name}'." + ) + + # Create credentials + credential = get_azure_credential( + managed_identity_client_id=managed_identity_client_id + ) + + # Copy blob file + async with BlobServiceClient( + account_url=f"https://{storage_domain_name}", + credential=credential, + ) as blob_service_client: + blob_client = blob_service_client.get_blob_client( + container=storage_container_name, + blob=storage_blob_name, + ) + blob_properties = await blob_client.get_blob_properties() + + return blob_properties diff --git a/code/function/tests/__init__.py b/code/function/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/function/tests/test_sample.py b/code/function/tests/test_sample.py new file mode 100644 index 0000000..6322d0f --- /dev/null +++ b/code/function/tests/test_sample.py @@ -0,0 +1,13 @@ +import pytest + + +def test_get_guid(): + # init + a = 1 + b = 2 + + # act + a += 1 + + # validate + assert a == b diff --git a/code/infra/applicationinsights.tf b/code/infra/applicationinsights.tf index babc33f..8173fca 100644 --- a/code/infra/applicationinsights.tf +++ b/code/infra/applicationinsights.tf @@ -5,7 +5,7 @@ module "application_insights" { } location = var.location - resource_group_name = azurerm_resource_group.resource_group_consumption.name + resource_group_name = azurerm_resource_group.resource_group_monitoring.name tags = var.tags application_insights_name = "${local.prefix}-appi001" application_insights_application_type = "web" diff --git a/code/infra/appserviceplan.tf b/code/infra/appserviceplan.tf index 8e9d07b..8e07286 100644 --- a/code/infra/appserviceplan.tf +++ b/code/infra/appserviceplan.tf @@ -1,4 +1,4 @@ -module "app_service_plan" { +module "app_service_plan_consumption" { source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/appserviceplan?ref=main" providers = { azurerm = azurerm @@ -16,3 +16,22 @@ module "app_service_plan" { service_plan_zone_balancing_enabled = false diagnostics_configurations = local.diagnostics_configurations } + +module "app_service_plan_ingestion" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/appserviceplan?ref=main" + providers = { + azurerm = azurerm + } + + location = var.location + resource_group_name = azurerm_resource_group.resource_group_ingestion.name + tags = var.tags + service_plan_name = "${local.prefix}-ngst-asp001" + service_plan_maximum_elastic_worker_count = null + service_plan_os_type = "Linux" + service_plan_per_site_scaling_enabled = false + service_plan_sku_name = "FC1" + service_plan_worker_count = 1 + service_plan_zone_balancing_enabled = false + # diagnostics_configurations = local.diagnostics_configurations +} diff --git a/code/infra/botservice.tf b/code/infra/botservice.tf index 1180767..02e37ac 100644 --- a/code/infra/botservice.tf +++ b/code/infra/botservice.tf @@ -16,9 +16,9 @@ module "bot_service" { key = null } bot_service_microsoft_app = { - app_id = module.user_assigned_identity.user_assigned_identity_client_id - app_msi_id = module.user_assigned_identity.user_assigned_identity_id - app_tenant_id = module.user_assigned_identity.user_assigned_identity_tenant_id + app_id = module.user_assigned_identity_consumption.user_assigned_identity_client_id + app_msi_id = module.user_assigned_identity_consumption.user_assigned_identity_id + app_tenant_id = module.user_assigned_identity_consumption.user_assigned_identity_tenant_id app_type = "UserAssignedMSI" } bot_service_sku = "S1" diff --git a/code/infra/functionapplinux.tf b/code/infra/functionapplinux.tf new file mode 100644 index 0000000..0f9a286 --- /dev/null +++ b/code/infra/functionapplinux.tf @@ -0,0 +1,196 @@ +resource "azapi_resource" "linux_function_app" { + type = "Microsoft.Web/sites@2024-04-01" + name = "${local.prefix}-fctn001" + location = var.location + parent_id = azurerm_resource_group.resource_group_ingestion.id + tags = var.tags + identity { + type = "UserAssigned" + identity_ids = [ + module.user_assigned_identity_ingestion.user_assigned_identity_id, + ] + } + + body = { + kind = "functionapp,linux", + properties = { + # autoGeneratedDomainNameLabelScope = "TenantReuse" + # clientAffinityEnabled = false + clientCertEnabled = false + clientCertExclusionPaths = null + clientCertMode = "Required" + # dailyMemoryTimeQuota = 0 + # daprConfig = null + # dnsConfiguration = {} + enabled = true + # endToEndEncryptionEnabled = true + functionAppConfig = { + deployment = { + storage = { + authentication = { + type = "UserAssignedIdentity" + userAssignedIdentityResourceId = module.user_assigned_identity_ingestion.user_assigned_identity_id + } + type = "blobContainer" + value = "${module.storage_account_function.storage_account_primary_blob_endpoint}${local.storage_account_container_function_code_name}" + } + } + runtime = { + name = "python" + version = "3.11" + } + scaleAndConcurrency = { + # alwaysReady = [] + instanceMemoryMB = 2048 + maximumInstanceCount = 40 + # triggers = { + # http = { + # perInstanceConcurrency = 5 + # } + # } + } + } + # hostNamesDisabled = false + # hostNameSslStates = [] + httpsOnly = true + # hyperV = false + # ipMode = "IPv4" + keyVaultReferenceIdentity = module.user_assigned_identity_ingestion.user_assigned_identity_id + publicNetworkAccess = "Enabled" + # redundancyMode = null + # redundancyMode = "None" + # reserved = true + # scmSiteAlsoStopped = false + serverFarmId = module.app_service_plan_ingestion.service_plan_id + # storageAccountRequired = false + virtualNetworkSubnetId = azapi_resource.subnet_function.id + siteConfig = { + appSettings = [ + { + name = "AzureWebJobsStorage__accountName" + value = module.storage_account_function.storage_account_name + }, + { + name = "AzureWebJobsStorage__credential" + value = "managedidentity" + }, + { + name = "AzureWebJobsStorage__clientId" + value = module.user_assigned_identity_ingestion.user_assigned_identity_client_id + }, + { + name = "APPLICATIONINSIGHTS_CONNECTION_STRING" + value = module.application_insights.application_insights_connection_string + }, + { + name = "MANAGED_IDENTITY_CLIENT_ID" + value = module.user_assigned_identity_ingestion.user_assigned_identity_client_id + }, + { + name = "AzureWebJobsSecretStorageType" + value = "keyvault" + }, + { + name = "AzureWebJobsSecretStorageKeyVaultUri" + value = module.key_vault_ingestion.key_vault_uri + }, + { + name = "AzureWebJobsSecretStorageKeyVaultClientId" + value = module.user_assigned_identity_ingestion.user_assigned_identity_client_id + }, + { + name = "AZURE_FUNCTIONS_ENVIRONMENT" + value = "Production" + }, + ] + # autoSwapSlotName = "" + # detailedErrorLoggingEnabled = true + # functionsRuntimeScaleMonitoringEnabled = null # Not available for flex plans + # healthCheckPath = null + http20Enabled = false # true + # httpLoggingEnabled = true + ipSecurityRestrictions = [] + ipSecurityRestrictionsDefaultAction = "Allow" # "Deny" + keyVaultReferenceIdentity = module.user_assigned_identity_consumption.user_assigned_identity_id + # loadBalancing = "LeastRequests" + # localMySqlEnabled = false + # minTlsCipherSuite = "" + minTlsVersion = "1.2" + publicNetworkAccess = "Enabled" + # remoteDebuggingEnabled = false + # requestTracingEnabled = true + scmIpSecurityRestrictions = [] + scmIpSecurityRestrictionsDefaultAction = "Allow" # "Deny" + scmIpSecurityRestrictionsUseMain = false + scmMinTlsVersion = "1.2" + # scmType = "None" + # websiteTimeZone = "UTC" + # webSocketsEnabled = false + } + } + } + + response_export_values = ["*"] + schema_validation_enabled = true + locks = [] + ignore_casing = false + ignore_missing_property = false +} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_linux_function_app" { + resource_id = azapi_resource.linux_function_app.id +} + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_linux_function_app" { + name = "logAnalytics" + target_resource_id = azapi_resource.linux_function_app.id + log_analytics_workspace_id = data.azurerm_log_analytics_workspace.log_analytics_workspace.id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_linux_function_app.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_linux_function_app.metrics + content { + category = entry.value + enabled = true + } + } +} + +resource "azurerm_private_endpoint" "linux_function_app_private_endpoint" { + name = "${azapi_resource.linux_function_app.name}-pe" + location = var.location + resource_group_name = azurerm_resource_group.resource_group_ingestion.name + tags = var.tags + + custom_network_interface_name = "${azapi_resource.linux_function_app.name}-nic" + private_service_connection { + name = "${azapi_resource.linux_function_app.name}-pe" + is_manual_connection = false + private_connection_resource_id = azapi_resource.linux_function_app.id + subresource_names = ["sites"] + } + subnet_id = azapi_resource.subnet_private_endpoints.id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_sites == "" ? [] : [1] + content { + name = "${azapi_resource.linux_function_app.name}-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_sites + ] + } + } + + lifecycle { + ignore_changes = [ + private_dns_zone_group + ] + } +} diff --git a/code/infra/locals.tf b/code/infra/locals.tf index 3091575..bebfb68 100644 --- a/code/infra/locals.tf +++ b/code/infra/locals.tf @@ -2,6 +2,7 @@ locals { # Naming locals prefix = "${lower(var.prefix)}-${var.environment}" resource_providers_to_register = [ + "Microsoft.App", "Microsoft.Authorization", "Microsoft.BotService", "Microsoft.CognitiveServices", @@ -22,11 +23,11 @@ locals { WEBSITE_CONTENTOVERVNET = "1" # Auth app settings - MICROSOFT_APP_ID = module.user_assigned_identity.user_assigned_identity_client_id + MICROSOFT_APP_ID = module.user_assigned_identity_consumption.user_assigned_identity_client_id MICROSOFT_APP_PASSWORD = "" - MICROSOFT_APP_TENANTID = module.user_assigned_identity.user_assigned_identity_tenant_id + MICROSOFT_APP_TENANTID = module.user_assigned_identity_consumption.user_assigned_identity_tenant_id MICROSOFT_APP_TYPE = "UserAssignedMSI" - MANAGED_IDENTITY_CLIENT_ID = module.user_assigned_identity.user_assigned_identity_client_id + MANAGED_IDENTITY_CLIENT_ID = module.user_assigned_identity_consumption.user_assigned_identity_client_id OAUTH_CONNECTION_NAME = local.bot_connection_aadv2_oauth_name # Azure open ai app settings @@ -64,8 +65,9 @@ locals { } # Storage locals - storage_account_container_raw_name = "raw" - storage_account_container_curated_name = "curated" + storage_account_container_raw_name = "raw" + storage_account_container_curated_name = "curated" + storage_account_container_function_code_name = "code" # Logging locals diagnostics_configurations = [ diff --git a/code/infra/main.tf b/code/infra/main.tf index 3957540..7640117 100644 --- a/code/infra/main.tf +++ b/code/infra/main.tf @@ -9,3 +9,9 @@ resource "azurerm_resource_group" "resource_group_ingestion" { location = var.location tags = var.tags } + +resource "azurerm_resource_group" "resource_group_monitoring" { + name = "${local.prefix}-bot-mntrng-rg" + location = var.location + tags = var.tags +} diff --git a/code/infra/network.tf b/code/infra/network.tf index 058eef2..3b33238 100644 --- a/code/infra/network.tf +++ b/code/infra/network.tf @@ -56,3 +56,34 @@ resource "azapi_resource" "subnet_private_endpoints" { azapi_resource.subnet_web_app ] } + +resource "azapi_resource" "subnet_function" { + type = "Microsoft.Network/virtualNetworks/subnets@2022-07-01" + name = "FunctionAppSubnetBot" + parent_id = data.azurerm_virtual_network.virtual_network.id + + body = { + properties = { + addressPrefix = var.subnet_cidr_function + delegations = [ + { + name = "FunctionDelegation" + properties = { + serviceName = "Microsoft.App/environments" + } + } + ] + ipAllocations = [] + networkSecurityGroup = { + id = data.azurerm_network_security_group.network_security_group.id + } + privateEndpointNetworkPolicies = "Enabled" + privateLinkServiceNetworkPolicies = "Enabled" + routeTable = { + id = data.azurerm_route_table.route_table.id + } + serviceEndpointPolicies = [] + serviceEndpoints = [] + } + } +} diff --git a/code/infra/roleassignments_uai.tf b/code/infra/roleassignments_uai_cnsm.tf similarity index 58% rename from code/infra/roleassignments_uai.tf rename to code/infra/roleassignments_uai_cnsm.tf index 2ac879e..c95511c 100644 --- a/code/infra/roleassignments_uai.tf +++ b/code/infra/roleassignments_uai_cnsm.tf @@ -1,15 +1,15 @@ -resource "azurerm_role_assignment" "uai_roleassignment_open_ai_contributor" { +resource "azurerm_role_assignment" "uai_cnsm_roleassignment_open_ai_contributor" { description = "Required for accessing azure open ai from the web app." scope = module.azure_open_ai.cognitive_account_id role_definition_name = "Cognitive Services OpenAI Contributor" - principal_id = module.user_assigned_identity.user_assigned_identity_principal_id + principal_id = module.user_assigned_identity_consumption.user_assigned_identity_principal_id principal_type = "ServicePrincipal" } -resource "azurerm_role_assignment" "uai_roleassignment_key_vault_secrets_user" { +resource "azurerm_role_assignment" "uai_cnsm_roleassignment_key_vault_secrets_user" { description = "Required for accessing secrets in the key vault from teh web app app settings." scope = module.key_vault_consumption.key_vault_id role_definition_name = "Key Vault Secrets User" - principal_id = module.user_assigned_identity.user_assigned_identity_principal_id + principal_id = module.user_assigned_identity_consumption.user_assigned_identity_principal_id principal_type = "ServicePrincipal" } diff --git a/code/infra/roleassignments_uai_ngst.tf b/code/infra/roleassignments_uai_ngst.tf new file mode 100644 index 0000000..aca69ab --- /dev/null +++ b/code/infra/roleassignments_uai_ngst.tf @@ -0,0 +1,39 @@ +resource "azurerm_role_assignment" "uai_ngst_roleassignment_storage_blob_data_owner" { + description = "Required for reading and writing data from the function." + scope = module.storage_account.storage_account_id + role_definition_name = "Storage Blob Data Owner" + principal_id = module.user_assigned_identity_ingestion.user_assigned_identity_principal_id + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "uai_ngst_roleassignment_storage_function_blob_data_owner" { + description = "Required for running the function." + scope = module.storage_account_function.storage_account_id + role_definition_name = "Storage Blob Data Owner" + principal_id = module.user_assigned_identity_ingestion.user_assigned_identity_principal_id + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "uai_ngst_roleassignment_storage_function_blob_data_contributor" { + description = "Required for running the function." + scope = module.storage_account_function.storage_account_id + role_definition_name = "Storage Blob Data Contributor" + principal_id = module.user_assigned_identity_ingestion.user_assigned_identity_principal_id + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "uai_ngst_roleassignment_open_ai_contributor" { + description = "Required for accessing azure open ai from the function." + scope = module.azure_open_ai.cognitive_account_id + role_definition_name = "Cognitive Services OpenAI Contributor" + principal_id = module.user_assigned_identity_ingestion.user_assigned_identity_principal_id + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "uai_ngst_roleassignment_key_vault_secrets_officer" { + description = "Required for reading and writing secrets in the key vault from the function." + scope = module.key_vault_ingestion.key_vault_id + role_definition_name = "Key Vault Secrets Officer" + principal_id = module.user_assigned_identity_ingestion.user_assigned_identity_principal_id + principal_type = "ServicePrincipal" +} diff --git a/code/infra/storage_functionflex.tf b/code/infra/storage_functionflex.tf new file mode 100644 index 0000000..1f6135a --- /dev/null +++ b/code/infra/storage_functionflex.tf @@ -0,0 +1,46 @@ +module "storage_account_function" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/storage?ref=main" + providers = { + azurerm = azurerm + time = time + } + + location = var.location + resource_group_name = azurerm_resource_group.resource_group_ingestion.name + tags = var.tags + + storage_account_name = replace("${local.prefix}-flxfnc-stg001", "-", "") + storage_access_tier = "Hot" + storage_account_type = "StorageV2" + storage_account_tier = "Standard" + storage_account_replication_type = "ZRS" + storage_account_allowed_copy_scope = "AAD" + storage_blob_change_feed_enabled = false + storage_blob_container_delete_retention_in_days = 7 + storage_blob_delete_retention_in_days = 7 + storage_blob_cors_rules = {} + storage_blob_last_access_time_enabled = false + storage_blob_versioning_enabled = false + storage_is_hns_enabled = false + storage_network_bypass = ["None"] + storage_network_private_link_access = [ + "/subscriptions/${data.azurerm_client_config.current.subscription_id}/providers/Microsoft.Security/datascanners/storageDataScanner", + ] + storage_public_network_access_enabled = true + storage_nfsv3_enabled = false + storage_sftp_enabled = false + storage_shared_access_key_enabled = false + storage_container_names = [local.storage_account_container_function_code_name] + storage_static_website = [] + diagnostics_configurations = local.diagnostics_configurations + subnet_id = azapi_resource.subnet_private_endpoints.id + connectivity_delay_in_seconds = var.connectivity_delay_in_seconds + private_endpoint_subresource_names = ["blob", "file"] + private_dns_zone_id_blob = var.private_dns_zone_id_blob + private_dns_zone_id_file = var.private_dns_zone_id_file + private_dns_zone_id_table = "" + private_dns_zone_id_queue = "" + private_dns_zone_id_web = "" + private_dns_zone_id_dfs = "" + customer_managed_key = local.customer_managed_key +} diff --git a/code/infra/userassignedidentity.tf b/code/infra/userassignedidentity.tf index 4e146f6..7339852 100644 --- a/code/infra/userassignedidentity.tf +++ b/code/infra/userassignedidentity.tf @@ -1,4 +1,4 @@ -module "user_assigned_identity" { +module "user_assigned_identity_consumption" { source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/userassignedidentity?ref=main" providers = { azurerm = azurerm @@ -10,3 +10,16 @@ module "user_assigned_identity" { user_assigned_identity_name = "${local.prefix}-uai001" user_assigned_identity_federated_identity_credentials = {} } + +module "user_assigned_identity_ingestion" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/userassignedidentity?ref=main" + providers = { + azurerm = azurerm + } + + location = var.location + resource_group_name = azurerm_resource_group.resource_group_ingestion.name + tags = var.tags + user_assigned_identity_name = "${local.prefix}-ngst-uai001" + user_assigned_identity_federated_identity_credentials = {} +} diff --git a/code/infra/variables.tf b/code/infra/variables.tf index 3ae304f..86f707f 100644 --- a/code/infra/variables.tf +++ b/code/infra/variables.tf @@ -138,6 +138,16 @@ variable "subnet_cidr_private_endpoints" { } } +variable "subnet_cidr_function" { + description = "Specifies the subnet cidr range for the function subnet." + type = string + sensitive = false + validation { + condition = length(split("/", var.subnet_cidr_function)) == 2 + error_message = "Please specify a valid subnet cidr range." + } +} + # DNS variables variable "private_dns_zone_id_vault" { description = "Specifies the resource ID of the private DNS zone for Azure Key Vault. Not required if DNS A-records get created via Azure Policy." @@ -216,6 +226,17 @@ variable "private_dns_zone_id_blob" { } } +variable "private_dns_zone_id_file" { + description = "Specifies the resource ID of the private DNS zone for file storage. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_file == "" || (length(split("/", var.private_dns_zone_id_file)) == 9 && endswith(var.private_dns_zone_id_file, "privatelink.file.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + variable "private_dns_zone_id_data_factory" { description = "Specifies the resource ID of the private DNS zone for Azure Data Factory. Not required if DNS A-records get created via Azure Policy." type = string diff --git a/code/infra/webapplinux.tf b/code/infra/webapplinux.tf index 2d007ff..2f8f7b8 100644 --- a/code/infra/webapplinux.tf +++ b/code/infra/webapplinux.tf @@ -6,7 +6,7 @@ resource "azurerm_linux_web_app" "linux_web_app" { identity { type = "UserAssigned" identity_ids = [ - module.user_assigned_identity.user_assigned_identity_id + module.user_assigned_identity_consumption.user_assigned_identity_id ] } @@ -18,9 +18,9 @@ resource "azurerm_linux_web_app" "linux_web_app" { enabled = true ftp_publish_basic_authentication_enabled = false https_only = true - key_vault_reference_identity_id = module.user_assigned_identity.user_assigned_identity_id + key_vault_reference_identity_id = module.user_assigned_identity_consumption.user_assigned_identity_id public_network_access_enabled = true - service_plan_id = module.app_service_plan.service_plan_id + service_plan_id = module.app_service_plan_consumption.service_plan_id virtual_network_subnet_id = azapi_resource.subnet_web_app.id webdeploy_publish_basic_authentication_enabled = false site_config { diff --git a/config/PerfectThymeTech/vars.tfvars b/config/PerfectThymeTech/vars.tfvars index 5215acf..9fb71ca 100644 --- a/config/PerfectThymeTech/vars.tfvars +++ b/config/PerfectThymeTech/vars.tfvars @@ -25,6 +25,7 @@ nsg_id = "/subscriptions/1fdab118-1638-419a-8b12-06c95437 route_table_id = "/subscriptions/1fdab118-1638-419a-8b12-06c9543714a0/resourceGroups/ptt-dev-networking-rg/providers/Microsoft.Network/routeTables/ptt-dev-default-rt001" subnet_cidr_web_app = "10.3.1.192/26" subnet_cidr_private_endpoints = "10.3.2.0/26" +subnet_cidr_function = "10.3.1.128/26" # DNS variables private_dns_zone_id_vault = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net" @@ -34,4 +35,5 @@ private_dns_zone_id_bot_framework_token = "/subscriptions/e82c5267-9dc4-4f4 private_dns_zone_id_open_ai = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.openai.azure.com" private_dns_zone_id_cosmos_sql = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.documents.azure.com" private_dns_zone_id_blob = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net" +private_dns_zone_id_file = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.file.core.windows.net" private_dns_zone_id_data_factory = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.datafactory.azure.net" diff --git a/docs/prereqs/locals.tf b/docs/prereqs/locals.tf index 9149b39..05d91f6 100644 --- a/docs/prereqs/locals.tf +++ b/docs/prereqs/locals.tf @@ -28,6 +28,7 @@ locals { open_ai = "privatelink.openai.azure.com", cosmos_sql = "privatelink.documents.azure.com", blob = "privatelink.blob.core.windows.net", + file = "privatelink.file.core.windows.net", data_factory = "privatelink.datafactory.azure.net", } }