我使用 Python (3.9) 创建了一些 Azure Functions,但在代码所在的 GitLab 存储库中设置 CI/CD 时遇到问题。该存储库驻留在 Azure VM 中,不确定这是否相关。这些函数连接到 Azure PostgreSQL 数据库,您将在代码中进一步看到。
.gitlab-ci.yml 触发 3 个作业:前 2 个作业成功构建 React 应用程序并将其部署到 Azure 静态 Web 应用程序。第三个在 GitLab 管道中成功运行,但之后我在 Azure Function App 概述页面中看不到任何函数。我也没有在作业中看到任何错误日志。
当使用 VSCode 扩展 Azure Functions Core Tools 直接部署到 Azure Function App 时,部署成功,函数显示在概述页面中,并且它们工作正常,连接到数据库和所有内容。我还可以在本地运行和测试这些功能。
以下是涉及的不同代码段。出于隐私考虑,某些信息将被省略,例如 URL
.gitlab-ci.yml:
stages:
- build-react
- deploy-react
- deploy-functions
# Build the React app
build-react:
stage: build-react
image: registry.gitlab.com/static-web-apps/azure-static-web-apps-deploy
script:
- cd app-frontend
- npm install
- VITE_FUNCTIONS_BASE_URL=azurewebsites-url npx vite build
artifacts:
paths:
- app-frontend/dist
only:
- main
# Deploy the React app to Azure Static Web Apps
deploy-react:
stage: deploy-react
image: registry.gitlab.com/static-web-apps/azure-static-web-apps-deploy
script:
- cd app-frontend
- npm install -g @azure/static-web-apps-cli
- swa deploy --env production --deployment-token token --output-location ./dist
only:
- main
deploy-functions:
stage: deploy-functions
image: mcr.microsoft.com/azure-functions/python:3.0 # Azure Functions image
script:
- set -e
- cd azure_functions # Navigate to the folder containing your Azure Function app
- zip -r ../my-functions.zip . # Create a zip file of only the contents of the azure_functions folder
- cd .. # Navigate back to the root directory
- echo "Zipped successfully"
- apt-get update && apt-get install -y ca-certificates curl apt-transport-https lsb-release gnupg
- curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- echo "Deploying to Azure..."
- az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID
- az functionapp deployment source config-zip --resource-group $AZURE_RESOURCE_GROUP --name $AZURE_FUNCTIONAPP_NAME --src ./my-functions.zip
only:
- main
需求.txt:
azure-functions
psycopg2-binary
function_app.py:
import json
import azure.functions as func
import logging
from datetime import datetime
import psycopg2
import os
# Database connection details
DB_HOST = os.getenv("DB_HOST")
DB_NAME = os.getenv("DB_NAME")
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_PORT = os.getenv("DB_PORT")
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@app.route(route="desks", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def getDesks(req: func.HttpRequest) -> func.HttpResponse:
# Connect to database and fetch reservations for the given date
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all desks
query = """
SELECT d.id
FROM desks d;
"""
cursor.execute(query)
desks = cursor.fetchall()
# Prepare the response data
if desks:
response_data = [{"id": desk[0]} for desk in desks]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse("No desks found", status_code=200)
except Exception as e:
logging.error(f"Error fetching desks: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(route="reservations", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def getReservationsForDate(req: func.HttpRequest) -> func.HttpResponse:
# Get the date from the query parameter (expected format: YYYY-MM-DD)
reservation_date = req.params.get("date")
if not reservation_date:
return func.HttpResponse(
"Please provide a 'date' query parameter (YYYY-MM-DD).", status_code=400
)
try:
# Convert string date to a date object for validation
reservation_date = datetime.strptime(reservation_date, "%Y-%m-%d").date()
except ValueError:
return func.HttpResponse(
"Invalid date format. Please provide a valid date in YYYY-MM-DD format.",
status_code=400,
)
# Connect to database and fetch reservations for the given date
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all reservations for the provided date
query = """
SELECT r.id, r.desk_id, r.reserved_by, r.reservation_date
FROM reservations r
WHERE r.reservation_date = %s;
"""
cursor.execute(query, (reservation_date,))
reservations = cursor.fetchall()
# Prepare the response data
if reservations:
response_data = [
{
"id": res[0],
"desk_id": res[1],
"reserved_by": res[2],
"reservation_date": res[3].strftime("%Y-%m-%d"),
}
for res in reservations
]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse(body=json.dumps([]), status_code=200)
except Exception as e:
logging.error(f"Error fetching reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(
route="reservation-dates", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS
)
def getDatesWithReservationForEmail(req: func.HttpRequest) -> func.HttpResponse:
email = req.params.get("email")
if not email:
return func.HttpResponse(
"Please provide an 'email' query parameter.", status_code=400
)
todays_date = datetime.today().date()
# Connect to database and fetch dates in which the email provided has reservations
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all reservations for the provided date
query = """
SELECT r.reservation_date
FROM reservations r
WHERE r.reserved_by = %s AND r.reservation_date >= %s;
"""
cursor.execute(query, (email, todays_date))
reservation_dates = cursor.fetchall()
# Prepare the response data
if reservation_dates:
response_data = [res[0].strftime("%Y-%m-%d") for res in reservation_dates]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse(body=json.dumps([]), status_code=200)
except Exception as e:
logging.error(f"Error fetching reservation dates for email {email}: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(route="reservations", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
def reserveDesks(req: func.HttpRequest) -> func.HttpResponse:
# Parse the JSON body
request_body = req.get_json()
if not request_body:
return func.HttpResponse("Invalid or missing JSON body.", status_code=400)
# Extract `userEmail`
employee_email = request_body.get("employee")
# Extract `reservations`
reservations = request_body.get("reservations", [])
# Validate presence of required fields
if not employee_email or not reservations:
return func.HttpResponse(
"Missing 'employee' or 'reservations' in the JSON body.",
status_code=400,
)
# Database connection
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
except Exception as e:
logging.error(f"Error connecting to the database: {str(e)}")
return func.HttpResponse("Database connection failed.", status_code=500)
# Connect to database and fetch reservations for the given date
try:
for reservation in reservations:
desk_id = reservation.get("desk_id")
reservation_date = reservation.get("reservation_date")
if not desk_id or not reservation_date:
return func.HttpResponse(
"Each reservation must include 'deskId' and 'reservationDate'.",
status_code=400,
)
checkIfSameReservationAlreadyExistsQuery = """
SELECT r.id, r.desk_id, r.reserved_by, r.reservation_date
FROM reservations r
WHERE r.reservation_date = %s AND r.desk_id = %s;
"""
cursor.execute(
checkIfSameReservationAlreadyExistsQuery, (reservation_date, desk_id)
)
reservations = cursor.fetchall()
if reservations and len(reservations) > 0:
response_data = [
{
"id": res[0],
"desk_id": res[1],
"reserved_by": res[2],
"reservation_date": res[3].strftime("%Y-%m-%d"),
}
for res in reservations
]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=409,
mimetype="application/json",
)
# Insert reservation into the database
insert_query = """
INSERT INTO reservations (desk_id, reserved_by, reservation_date)
VALUES (%s, %s, %s)
"""
cursor.execute(insert_query, (desk_id, employee_email, reservation_date))
# Commit the transaction
connection.commit()
except Exception as e:
logging.error(f"Error fetching reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Return success response
return func.HttpResponse(
f"Successfully created {len(reservations)} reservations for {employee_email}.",
status_code=201,
)
@app.route(
route="reservations", methods=["DELETE"], auth_level=func.AuthLevel.ANONYMOUS
)
def cancelReservations(req: func.HttpRequest) -> func.HttpResponse:
# Get reservation_ids from query parameter
reservation_ids_str = req.params.get("reservation_ids")
if not reservation_ids_str:
return func.HttpResponse(
"Please provide reservation_ids as a query parameter.", status_code=400
)
try:
# Split the reservation_ids and convert them into a list of integers
reservation_ids = list(map(int, reservation_ids_str.split(",")))
# Connect to database and delete the reservations
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
placeholders = ", ".join("%s" for _ in reservation_ids)
# Query to delete the reservations by IDs
query = f"""
DELETE FROM reservations
WHERE id IN ({placeholders});
"""
cursor.execute(query, reservation_ids)
connection.commit()
# Check how many rows were deleted
deleted_rows = cursor.rowcount
if deleted_rows > 0:
return func.HttpResponse(
f"Successfully deleted {deleted_rows} reservation(s).", status_code=200
)
else:
return func.HttpResponse(
"No reservations found with the provided IDs.", status_code=404
)
except Exception as e:
logging.error(f"Error deleting reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
下面是一个
.gitlab-ci.yml
文件示例,用于使用 npm 和 Azure 静态 Web 应用部署工作流程来部署项目。
variables:
API_TOKEN: $DEPLOYMENT_TOKEN
APP_PATH: '$CI_PROJECT_DIR'
OUTPUT_PATH: '$CI_PROJECT_DIR/build'
cache:
paths:
- node_modules/
- .yarn
stages:
- build
- deploy
build_job:
image: node:16
stage: build
script:
- npm install
- npm run build
artifacts:
paths:
- ./build
deploy_job:
environment: production
stage: deploy
image: mcr.microsoft.com/azure-static-web-apps-deploy:latest
script:
- echo "Deploying to Azure Static Web App..."
- npx azure-static-web-apps-deploy --app-location "/" --api-location "./api" --output-location "build" --token $API_TOKEN
我根据文件夹结构将
app_location
更改为 /
、api_location
更改为 ./api
、output_location
更改为 build
。根据您自己的文件夹结构更改这些值。
静态 Web 应用程序中 React 应用程序的输出: 静态 Web 应用程序中 Azure Function 应用程序 api 的输出::**
或者,您也可以仅使用
.yml
文件,例如 azure-static-web-apps.yml
,其中 app_location
设置为 /
,api_location
设置为 ./api
,output_location
设置为 build
,根据文件夹结构。请参阅我的SO 帖子了解更多详情。