Skip to content

Commit 93a714b

Browse files
authored
Merge pull request #6 from PerfectThymeTech/marvinbuss/docs
Update documentation
2 parents d342352 + 1e34510 commit 93a714b

27 files changed

+191
-51
lines changed

.github/workflows/functionApp.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ on:
55
- main
66
paths:
77
- "**.py"
8+
- "code/function/**"
89

910
pull_request:
1011
branches:
1112
- main
1213
paths:
1314
- "**.py"
15+
- "code/function/**"
1416

1517
jobs:
1618
function_test:
@@ -24,7 +26,7 @@ jobs:
2426
uses: ./.github/workflows/_functionAppDeployTemplate.yml
2527
name: "Function App Deploy"
2628
needs: [function_test]
27-
if: github.event_name == 'push' || github.event_name == 'release'
29+
# if: github.event_name == 'push' || github.event_name == 'release'
2830
with:
2931
environment: "dev"
3032
python_version: "3.10"

.github/workflows/terraform.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ on:
55
- main
66
paths:
77
- "**.tf"
8+
- "code/infra/**"
89

910
pull_request:
1011
branches:
1112
- main
1213
paths:
1314
- "**.tf"
15+
- "code/infra/**"
1416

1517
jobs:
1618
terraform_lint:

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,54 @@ This repository provides a scalable baseline for Azure Functions written in Pyth
55
1. A compliant infrastructure baseline written in Terraform,
66
2. A Python code baseline that follows best practices and
77
3. A safe rollout mechanism of code artifacts.
8+
9+
## Infrastructure
10+
11+
The infrastructure as code (IaC) is written in Terraform and uses all the latest and greatest Azure Function features to ensure high security standards and the lowest attack surface possible. The code can be found in the [`/code/infra` folder](/code/infra/) and creates the following resources:
12+
13+
* App Service Plan,
14+
* Azure Function,
15+
* Azure Storage Account,
16+
* Azure Key Vault,
17+
* Azure Application Insights and
18+
* Azure Log Analytics Workspace.
19+
20+
The Azure Function is configured in a way to fulfill highest compliance standards. In addition, the end-to-end setup takes care of wiring up all services to ensure a productive experience on day one. For instance, the Azure Function is automatically being connected to Azure Application Insights and the Application Insights service is being connected to the Azure Log Analytics Workspace.
21+
22+
### Network configuration
23+
24+
The deployed services ensure a compliant network setup using the following features:
25+
26+
* Public network access is denied for all services.
27+
* All deployed services rely on Azure Private Endpoints for all network flows including deployments and usage of the services.
28+
29+
### Authentication & Authorization
30+
31+
The deployed services ensure a compliant authentication & authorization setup using the following features:
32+
33+
* No key-based or local/basic authentication flows.
34+
* Azure AD-only authentication.
35+
* All authorization is controlled by Azure RBAC.
36+
* This includes the interaction of the Azure Function with the Azure Storage Account and the Azure Key Vault.
37+
38+
### Encryption
39+
40+
The deployed services ensure a compliant encryption setup using the following features:
41+
42+
* Encryption at rest using 256-bit AES (FIPS 140-2).
43+
* HTTPS traffic only.
44+
* All traffic is encrypted using TLS 1.2.
45+
* Note: Customer-manaed keys are not used at this point in time but can be added easily.
46+
* Note: Cypher suites are set to default and can further be limited.
47+
48+
## Azure Function Code
49+
50+
The Azure Function code is written in Python and leverages the new [Web Framework integration](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=asgi%2Capplication-level&pivots=python-mode-decorators#web-frameworks) supported by the v2 Python programming model. This allows to rely on proven frameworks such as FastAPI and Flask. The Azure Function application code can be found in the [`/code/function` folder](/code/function/).
51+
52+
## FastAPI
53+
54+
This sample uses FastAPI as a baseline which is a scalable, modern, fast and proven web framework for APIs built in Python. More details about FastAPI can be found [here](https://fastapi.tiangolo.com/).
55+
56+
## Testing
57+
58+
Testing of the Azure Functon application code. The testing is done using `pytest`. Tests are stored in the [`/tests` folder](/tests/) and should be extended for new functionality that is being implemented over time. The `pytest.ini` is used to reference the Azure Functon project for imports. This file makes sure that the respective python objects from the Azrue Function application code can be imported into the tests and validated accordingly.
File renamed without changes.

code/function/api/v1/api_v1.py renamed to code/function/fastapp/api/v1/api_v1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from fastapi import APIRouter
2-
from function.api.v1.endpoints import heartbeat, sample
2+
from fastapp.api.v1.endpoints import heartbeat, sample
33

44
api_v1_router = APIRouter()
55
api_v1_router.include_router(sample.router, prefix="/sample", tags=["sample"])

code/function/api/v1/endpoints/heartbeat.py renamed to code/function/fastapp/api/v1/endpoints/heartbeat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from typing import Any
22

33
from fastapi import APIRouter
4-
from function.models.heartbeat import HearbeatResult
5-
from function.utils import setup_logging
4+
from fastapp.models.heartbeat import HearbeatResult
5+
from fastapp.utils import setup_logging
66

77
logger = setup_logging(__name__)
88

code/function/api/v1/endpoints/sample.py renamed to code/function/fastapp/api/v1/endpoints/sample.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from typing import Any
22

33
from fastapi import APIRouter
4-
from function.models.sample import SampleRequest, SampleResponse
5-
from function.utils import setup_logging
4+
from fastapp.models.sample import SampleRequest, SampleResponse
5+
from fastapp.utils import setup_logging
66

77
logger = setup_logging(__name__)
88

code/function/core/config.py renamed to code/function/fastapp/core/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from pydantic import BaseSettings
3+
from pydantic import BaseSettings, Field
44

55

66
class Settings(BaseSettings):
@@ -10,6 +10,9 @@ class Settings(BaseSettings):
1010
API_V1_STR: str = "/v1"
1111
LOGGING_LEVEL: int = logging.INFO
1212
DEBUG: bool = False
13+
APPLICATIONINSIGHTS_CONNECTION_STRING: str = Field(
14+
default="", env="APPLICATIONINSIGHTS_CONNECTION_STRING"
15+
)
1316

1417

1518
settings = Settings()

code/function/fastapp/main.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from fastapi import FastAPI
2+
from fastapp.api.v1.api_v1 import api_v1_router
3+
from fastapp.core.config import settings
4+
5+
6+
def get_app() -> FastAPI:
7+
"""Setup the Fast API server.
8+
9+
RETURNS (FastAPI): The FastAPI object to start the server.
10+
"""
11+
app = FastAPI(
12+
title=settings.PROJECT_NAME,
13+
version=settings.APP_VERSION,
14+
openapi_url="/openapi.json",
15+
debug=settings.DEBUG,
16+
)
17+
app.include_router(api_v1_router, prefix=settings.API_V1_STR)
18+
return app
19+
20+
21+
app = get_app()
22+
23+
24+
@app.on_event("startup")
25+
async def startup_event():
26+
"""Gracefully start the application before the server reports readiness."""
27+
pass
28+
29+
30+
@app.on_event("shutdown")
31+
async def shutdown_event():
32+
"""Gracefully close connections before shutdown of the server."""
33+
pass

code/function/fastapp/models/__init__.py

Whitespace-only changes.

code/function/utils.py renamed to code/function/fastapp/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import logging
22
from logging import Logger
33

4-
from function.core.config import settings
4+
from fastapp.core.config import settings
55

66

77
def setup_logging(module) -> Logger:
88
"""Setup logging and event handler.
9+
910
RETURNS (Logger): The logger object to log activities.
1011
"""
1112
logger = logging.getLogger(module)
@@ -17,6 +18,5 @@ def setup_logging(module) -> Logger:
1718
logger_stream_handler.setFormatter(
1819
logging.Formatter("[%(asctime)s] [%(levelname)s] [%(module)-8.8s] %(message)s")
1920
)
20-
2121
logger.addHandler(logger_stream_handler)
2222
return logger

code/function/function_app.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

code/function/host.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
{
22
"version": "2.0",
33
"logging": {
4+
"fileLoggingMode": "debugOnly",
5+
"logLevel": {
6+
"default": "Information",
7+
"Host": "Information",
8+
"Function": "Information",
9+
"Host.Aggregator": "Information"
10+
},
411
"applicationInsights": {
512
"samplingSettings": {
613
"isEnabled": true,
7-
"excludedTypes": "Request"
14+
"excludedTypes": "Request;Exception"
815
}
916
}
1017
},
1118
"extensionBundle": {
1219
"id": "Microsoft.Azure.Functions.ExtensionBundle",
1320
"version": "[4.*, 5.0.0)"
21+
},
22+
"extensions": {
23+
"http": {
24+
"routePrefix": ""
25+
}
1426
}
1527
}

code/function/wrapper/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import azure.functions as func
2+
from fastapp.main import app
3+
4+
5+
async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
6+
return await func.AsgiMiddleware(app).handle_async(req, context)

code/function/wrapper/function.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"scriptFile": "__init__.py",
3+
"bindings": [
4+
{
5+
"authLevel": "anonymous",
6+
"type": "httpTrigger",
7+
"direction": "in",
8+
"name": "req",
9+
"methods": [
10+
"get",
11+
"post"
12+
],
13+
"route": "{*route}"
14+
},
15+
{
16+
"type": "http",
17+
"direction": "out",
18+
"name": "$return"
19+
}
20+
]
21+
}

code/infra/function.tf

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ resource "azapi_resource" "function" {
7070
},
7171
{
7272
name = "WEBSITE_RUN_FROM_PACKAGE"
73+
value = "0"
74+
},
75+
{
76+
name = "PYTHON_ENABLE_WORKER_EXTENSIONS"
77+
value = "1"
78+
},
79+
{
80+
name = "ENABLE_ORYX_BUILD"
81+
value = "1"
82+
},
83+
{
84+
name = "SCM_DO_BUILD_DURING_DEPLOYMENT"
7385
value = "1"
7486
},
7587
{
@@ -82,9 +94,10 @@ resource "azapi_resource" "function" {
8294
functionAppScaleLimit = 0
8395
functionsRuntimeScaleMonitoringEnabled = false
8496
ftpsState = "Disabled"
97+
healthCheckPath = var.function_health_path
8598
http20Enabled = false
8699
ipSecurityRestrictionsDefaultAction = "Deny"
87-
linuxFxVersion = "Python|${var.python_version}"
100+
linuxFxVersion = "Python|${var.function_python_version}"
88101
localMySqlEnabled = false
89102
loadBalancing = "LeastRequests"
90103
minTlsVersion = "1.2"

code/infra/logging.tf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,23 @@ resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_log_analytics_
101101
}
102102
}
103103
}
104+
105+
resource "azurerm_monitor_private_link_scope" "mpls" {
106+
name = "${local.prefix}-ampls001"
107+
resource_group_name = azurerm_resource_group.logging_rg.name
108+
tags = var.tags
109+
}
110+
111+
resource "azurerm_monitor_private_link_scoped_service" "mpls_application_insights" {
112+
name = "ampls-${azurerm_application_insights.application_insights.name}"
113+
resource_group_name = azurerm_monitor_private_link_scope.mpls.resource_group_name
114+
scope_name = azurerm_monitor_private_link_scope.mpls.name
115+
linked_resource_id = azurerm_application_insights.application_insights.id
116+
}
117+
118+
resource "azurerm_monitor_private_link_scoped_service" "mpls_log_analytics_workspace" {
119+
name = "ampls-${azurerm_log_analytics_workspace.log_analytics_workspace.name}"
120+
resource_group_name = azurerm_monitor_private_link_scope.mpls.resource_group_name
121+
scope_name = azurerm_monitor_private_link_scope.mpls.name
122+
linked_resource_id = azurerm_log_analytics_workspace.log_analytics_workspace.id
123+
}

code/infra/variables.tf

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,27 @@ variable "route_table_id" {
6262
}
6363
}
6464

65-
variable "python_version" {
65+
variable "function_python_version" {
6666
description = "Specifies the python version of the Azure Function."
6767
type = string
6868
sensitive = false
6969
default = "3.10"
7070
validation {
71-
condition = contains(["3.9", "3.10"], var.python_version)
71+
condition = contains(["3.9", "3.10"], var.function_python_version)
7272
error_message = "Please specify a valid Python version."
7373
}
7474
}
7575

76+
variable "function_health_path" {
77+
description = "Specifies the health endpoint of the Azure Function."
78+
type = string
79+
sensitive = false
80+
validation {
81+
condition = startswith(var.function_health_path, "/")
82+
error_message = "Please specify a valid path."
83+
}
84+
}
85+
7686
variable "private_dns_zone_id_blob" {
7787
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."
7888
type = string

code/infra/vars.dev.tfvars

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ location = "northeurope"
22
environment = "dev"
33
prefix = "myfunc"
44
tags = {}
5+
function_python_version = "3.10"
6+
function_health_path = "/v1/health/heartbeat"
57
vnet_id = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-function-network-rg/providers/Microsoft.Network/virtualNetworks/mycrp-prd-function-vnet001"
68
nsg_id = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-function-network-rg/providers/Microsoft.Network/networkSecurityGroups/mycrp-prd-function-nsg001"
79
route_table_id = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-function-network-rg/providers/Microsoft.Network/routeTables/mycrp-prd-function-rt001"

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[pytest]
2-
pythonpath = code
2+
pythonpath = code/function

0 commit comments

Comments
 (0)