Skip to content

Add Function Workflows #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/_functionAppDeployTemplate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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.10"
description: "Specifies the python version."
function_directory:
required: true
type: string
description: "Specifies the directory of the Azure Function."
function_name:
required: true
type: string
description: "Specifies the name of the Azure Function."
secrets:
TENANT_ID:
required: true
description: "Specifies the tenant id of the deployment."
CLIENT_ID:
required: true
description: "Specifies the client id."
CLIENT_SECRET:
required: true
description: "Specifies the client secret."
SUBSCRIPTION_ID:
required: true
description: "Specifies the client id."

jobs:
deployment:
name: Function App Deploy
runs-on: self-hosted
continue-on-error: false
environment: ${{ inputs.environment }}

steps:
# Setup Python 3.10
- name: Setup Python 3.10
id: python_setup
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python_version }}

# Check Out Repository
- name: Check Out Repository
id: checkout_repository
uses: actions/checkout@v3

# Install Function Dependencies
- name: Resolve Function Dependencies
id: function_dependencies
shell: bash
run: |
pushd '${{ inputs.function_directory }}'
python -m pip install --upgrade pip
pip install -r requirements.txt --target=".python_packages/lib/site-packages"
popd

# Login to Azure
- name: Azure Login
id: azure_login
uses: azure/login@v1
with:
creds: '{"clientId":"${{ secrets.CLIENT_ID }}","clientSecret":"${{ secrets.CLIENT_SECRET }}","subscriptionId":"${{ secrets.SUBSCRIPTION_ID }}","tenantId":"${{ secrets.TENANT_ID }}"}'

# Deploy Function
- name: Deploy Function
id: function_deploy
uses: Azure/functions-action@v1
with:
app-name: ${{ inputs.function_name }}
package: ${{ inputs.function_directory }}
scm-do-build-during-deployment: true
enable-oryx-build: true
41 changes: 41 additions & 0 deletions .github/workflows/_functionAppTestTemplate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Function App Test Template

on:
workflow_call:
inputs:
python_version:
required: true
type: string
default: "3.10"
description: "Specifies the python version."
function_directory:
required: true
type: string
description: "Specifies the directory of the Azure Function."

jobs:
deployment:
name: Function App Test
runs-on: ubuntu-latest
continue-on-error: false

steps:
# Setup Python 3.10
- name: Setup Python 3.10
id: python_setup
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python_version }}

# Check Out Repository
- name: Check Out Repository
id: checkout_repository
uses: actions/checkout@v3

# Run Python Tests
- name: Run Python Tests
id: python_test
run: |
pip install -r ${{ inputs.function_directory }}/requirements.txt -q
pip install -r requirements.txt -q
pytest
37 changes: 37 additions & 0 deletions .github/workflows/functionApp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Function App Deployment
on:
push:
branches:
- main
paths:
- "**.py"

pull_request:
branches:
- main
paths:
- "**.py"

jobs:
function_test:
uses: ./.github/workflows/_functionAppTestTemplate.yml
name: "Function App Test"
with:
python_version: "3.10"
function_directory: "./code/function"

function_deploy:
uses: ./.github/workflows/_functionAppDeployTemplate.yml
name: "Function App Deploy"
needs: [function_test]
if: github.event_name == 'push' || github.event_name == 'release'
with:
environment: "dev"
python_version: "3.10"
function_directory: "./code/function"
function_name: "myfunc-dev-fctn001"
secrets:
TENANT_ID: ${{ secrets.TENANT_ID }}
CLIENT_ID: ${{ secrets.CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
SUBSCRIPTION_ID: ${{ secrets.SUBSCRIPTION_ID }}
44 changes: 0 additions & 44 deletions .github/workflows/python.yml

This file was deleted.

2 changes: 1 addition & 1 deletion code/function/api/v1/api_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
from function.api.v1.endpoints import heartbeat, sample

api_v1_router = APIRouter()
api_v1_router.include_router(sample.router, prefix="/landingZone", tags=["sample"])
api_v1_router.include_router(sample.router, prefix="/sample", tags=["sample"])
api_v1_router.include_router(heartbeat.router, prefix="/health", tags=["health"])
4 changes: 2 additions & 2 deletions code/function/api/v1/endpoints/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
router = APIRouter()


@router.post("/create", response_model=SampleResponse, name="create")
@router.post("/sample", response_model=SampleResponse, name="sample")
async def post_predict(
data: SampleRequest,
) -> SampleResponse:
logger.info(f"Received request: {data}")
return SampleResponse(output=f"Hello ${data.input}")
return SampleResponse(output=f"Hello {data.input}")
33 changes: 30 additions & 3 deletions code/infra/function.tf
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ resource "azapi_resource" "function" {
scmSiteAlsoStopped = false
serverFarmId = azurerm_service_plan.service_plan.id
storageAccountRequired = false
vnetContentShareEnabled = true
virtualNetworkSubnetId = azapi_resource.subnet_function.id
vnetRouteAllEnabled = true
siteConfig = {
autoHealEnabled = false
acrUseManagedIdentityCreds = false
Expand All @@ -66,6 +68,10 @@ resource "azapi_resource" "function" {
name = "WEBSITE_CONTENTOVERVNET"
value = "1"
},
{
name = "WEBSITE_RUN_FROM_PACKAGE"
value = "1"
},
{
name = "AzureWebJobsStorage__accountName"
value = azurerm_storage_account.storage.name
Expand All @@ -75,10 +81,10 @@ resource "azapi_resource" "function" {
detailedErrorLoggingEnabled = true
functionAppScaleLimit = 0
functionsRuntimeScaleMonitoringEnabled = false
ftpsState = "FtpsOnly"
ftpsState = "Disabled"
http20Enabled = false
ipSecurityRestrictionsDefaultAction = "Deny"
linuxFxVersion = "Python|3.10"
linuxFxVersion = "Python|${var.python_version}"
localMySqlEnabled = false
loadBalancing = "LeastRequests"
minTlsVersion = "1.2"
Expand All @@ -89,7 +95,6 @@ resource "azapi_resource" "function" {
scmIpSecurityRestrictionsUseMain = false
scmIpSecurityRestrictionsDefaultAction = "Deny"
use32BitWorkerProcess = true
vnetRouteAllEnabled = true
vnetPrivatePortsCount = 0
webSocketsEnabled = false
}
Expand Down Expand Up @@ -131,3 +136,25 @@ resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_function" {
}
}
}

resource "azurerm_private_endpoint" "function_private_endpoint" {
name = "${azapi_resource.function.name}-pe"
location = var.location
resource_group_name = azurerm_resource_group.app_rg.name
tags = var.tags

custom_network_interface_name = "${azapi_resource.function.name}-nic"
private_service_connection {
name = "${azapi_resource.function.name}-pe"
is_manual_connection = false
private_connection_resource_id = azapi_resource.function.id
subresource_names = ["sites"]
}
subnet_id = azapi_resource.subnet_services.id
private_dns_zone_group {
name = "${azapi_resource.function.name}-arecord"
private_dns_zone_ids = [
var.private_dns_zone_id_sites
]
}
}
8 changes: 7 additions & 1 deletion code/infra/roleassignments.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
resource "azurerm_role_assignment" "role_assignment_storage_function" {
resource "azurerm_role_assignment" "function_role_assignment_storage" {
scope = azurerm_storage_account.storage.id
role_definition_name = "Storage Blob Data Owner"
principal_id = azapi_resource.function.identity[0].principal_id
}

resource "azurerm_role_assignment" "function_role_assignment_key_vault" {
scope = azurerm_key_vault.key_vault.id
role_definition_name = "Key Vault Secrets User"
principal_id = azapi_resource.function.identity[0].principal_id
}
28 changes: 25 additions & 3 deletions code/infra/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ variable "tags" {
}

variable "vnet_id" {
description = "Specifies the resource ID of the Vnet used for the Data Landing Zone"
description = "Specifies the resource ID of the Vnet used for the Azure Function."
type = string
sensitive = false
validation {
Expand All @@ -43,7 +43,7 @@ variable "vnet_id" {
}

variable "nsg_id" {
description = "Specifies the resource ID of the default network security group for the Data Landing Zone"
description = "Specifies the resource ID of the default network security group for the Azure Function."
type = string
sensitive = false
validation {
Expand All @@ -53,7 +53,7 @@ variable "nsg_id" {
}

variable "route_table_id" {
description = "Specifies the resource ID of the default route table for the Data Landing Zone"
description = "Specifies the resource ID of the default route table for the Azure Function."
type = string
sensitive = false
validation {
Expand All @@ -62,6 +62,17 @@ variable "route_table_id" {
}
}

variable "python_version" {
description = "Specifies the python version of the Azure Function."
type = string
sensitive = false
default = "3.10"
validation {
condition = contains(["3.9", "3.10"], var.python_version)
error_message = "Please specify a valid Python version."
}
}

variable "private_dns_zone_id_blob" {
description = "Specifies the resource ID of the private DNS zone for Azure Storage blob endpoints. Not required if DNS A-records get created via Azue Policy."
type = string
Expand Down Expand Up @@ -116,3 +127,14 @@ variable "private_dns_zone_id_key_vault" {
error_message = "Please specify a valid resource ID for the private DNS Zone."
}
}

variable "private_dns_zone_id_sites" {
description = "Specifies the resource ID of the private DNS zone for Azure Websites. Not required if DNS A-records get created via Azue Policy."
type = string
sensitive = false
default = ""
validation {
condition = var.private_dns_zone_id_sites == "" || (length(split("/", var.private_dns_zone_id_sites)) == 9 && endswith(var.private_dns_zone_id_sites, "privatelink.azurewebsites.net"))
error_message = "Please specify a valid resource ID for the private DNS Zone."
}
}
1 change: 1 addition & 0 deletions code/infra/vars.dev.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ private_dns_zone_id_queue = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1
private_dns_zone_id_table = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.table.core.windows.net"
private_dns_zone_id_file = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.file.core.windows.net"
private_dns_zone_id_key_vault = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net"
private_dns_zone_id_sites = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.azurewebsites.net"
13 changes: 13 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@ def test_get_heartbeat(client, version):
# assert
assert response.status_code == 200
assert response.json() == {"isAlive": True}


@pytest.mark.parametrize("version", ("v1",))
def test_post_sample(client, version):
# arrange
path = f"/{version}/sample/sample"

# action
response = client.post(path, json={"input": "Test"})

# assert
assert response.status_code == 200
assert response.json() == {"output": "Hello Test"}