diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..3aa51acb2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +*.pyc +__pycache__ +*.pyo +*.pyd +.Python +env/ +venv/ +.env +.venv +pip-log.txt +pip-delete-this-directory.txt +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.log +.pytest_cache/ +.git +.gitignore +.github +README.md +CHANGELOG.md +CONTRIBUTING.md +LICENSE.md +tests/ +docs/ +*.md \ No newline at end of file diff --git a/.github/workflows/deploy-infrastructure.yml b/.github/workflows/deploy-infrastructure.yml new file mode 100644 index 000000000..3142eafc9 --- /dev/null +++ b/.github/workflows/deploy-infrastructure.yml @@ -0,0 +1,36 @@ +name: Deploy Infrastructure + +on: + push: + branches: + - main + paths: + - 'infra/**' + workflow_dispatch: + +permissions: + id-token: write + contents: read + +env: + RESOURCE_GROUP: aguadamillas_students_1 + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Azure Login + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy Bicep + uses: azure/arm-deploy@v1 + with: + resourceGroupName: ${{ env.RESOURCE_GROUP }} + template: ./infra/main.bicep + parameters: ./infra/main.parameters.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..b7ed97095 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Flask in Docker", + "type": "python", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app" + } + ], + "python": { + "pythonPath": "python", + "venvPath": "", + "venvName": "" + } + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..95d352cf5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Use official Python runtime as a parent image +FROM python:3.11-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PORT=8000 + +# Set work directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first to leverage Docker cache +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application +COPY . . + +# Create a non-root user and switch to it +RUN adduser --disabled-password --gecos '' appuser \ + && chown -R appuser:appuser /app +USER appuser + +# Expose the port the app runs on +EXPOSE 8000 + +# Command to run the application +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ec4092c3b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + web: + build: . + ports: + - "8000:8000" + volumes: + - .:/app + environment: + - FLASK_APP=app.py + - FLASK_ENV=development + - PYTHONUNBUFFERED=1 + command: gunicorn --bind 0.0.0.0:8000 --reload app:app \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep index 391f0103f..e16bd3258 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -9,9 +9,8 @@ targetScope = 'subscription' @description('Name of the the environment which is used to generate a short unique hash used in all resources.') param environmentName string -@minLength(1) -@description('Primary location for all resources') -param location string +@description('The location for all resources') +param location string = resourceGroup().location // Optional parameters to override the default azd resource naming conventions. // Add the following to main.parameters.json to provide values: @@ -20,7 +19,16 @@ param location string // } param resourceGroupName string = '' param appServiceName string = '' -param appServicePlanName string = '' +@description('The name of the container registry') +param acrName string +@description('The name of the app service plan') +param appServicePlanName string +@description('The name of the web app') +param webAppName string +@description('The name of the container image') +param containerImageName string +@description('The version/tag of the container image') +param containerImageVersion string var abbrs = loadJsonContent('./abbreviations.json') @@ -50,31 +58,54 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { // Add resources to be provisioned below. -// The application App -module web './core/host/appservice.bicep' = { - name: 'web' - scope: rg +// Deploy Azure Container Registry +module acr 'modules/acr.bicep' = { + name: 'acr-deployment' + scope: resourceGroup() params: { - name: !empty(appServiceName) ? appServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' + name: acrName location: location - appServicePlanId: appServicePlan.outputs.id - runtimeName: 'python' - runtimeVersion: '3.13' - scmDoBuildDuringDeployment: true - tags: union(tags, { 'azd-service-name': 'web' }) + acrAdminUserEnabled: true } } -// Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan './core/host/appserviceplan.bicep' = { - name: 'appserviceplan' - scope: rg +// Deploy App Service Plan +module appServicePlan 'modules/app-service-plan.bicep' = { + name: 'app-service-plan-deployment' + scope: resourceGroup() params: { - name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' + name: appServicePlanName location: location - tags: tags sku: { + capacity: 1 + family: 'B' name: 'B1' + size: 'B1' + tier: 'Basic' + } + kind: 'Linux' + reserved: true + } +} + +// Deploy Web App +module webApp 'modules/web-app.bicep' = { + name: 'web-app-deployment' + scope: resourceGroup() + params: { + name: webAppName + location: location + kind: 'app' + serverFarmResourceId: appServicePlan.outputs.id + siteConfig: { + linuxFxVersion: 'DOCKER|${acr.outputs.loginServer}/${containerImageName}:${containerImageVersion}' + appCommandLine: '' + } + appSettingsKeyValuePairs: { + WEBSITES_ENABLE_APP_SERVICE_STORAGE: 'false' + DOCKER_REGISTRY_SERVER_URL: 'https://${acr.outputs.loginServer}' + DOCKER_REGISTRY_SERVER_USERNAME: acr.outputs.adminUsername + DOCKER_REGISTRY_SERVER_PASSWORD: acr.outputs.adminPassword } } } @@ -89,4 +120,5 @@ module appServicePlan './core/host/appserviceplan.bicep' = { // To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId +output webAppHostName string = webApp.outputs.defaultHostName diff --git a/infra/main.parameters.json b/infra/main.parameters.json index f1600cfbc..d992089e2 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -2,11 +2,23 @@ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { - "environmentName": { - "value": "${AZURE_ENV_NAME}" - }, "location": { - "value": "${AZURE_LOCATION}" + "value": "eastus" + }, + "acrName": { + "value": "zeidpractice3acr" + }, + "appServicePlanName": { + "value": "zeidpractice3-plan" + }, + "webAppName": { + "value": "zeidpractice3-webapp" + }, + "containerImageName": { + "value": "flask-app" + }, + "containerImageVersion": { + "value": "latest" } } } diff --git a/infra/modules/acr.bicep b/infra/modules/acr.bicep new file mode 100644 index 000000000..355652189 --- /dev/null +++ b/infra/modules/acr.bicep @@ -0,0 +1,23 @@ +@description('The name of the container registry') +param name string + +@description('The location of the container registry') +param location string + +@description('Enable admin user') +param acrAdminUserEnabled bool = true + +resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: name + location: location + sku: { + name: 'Basic' + } + properties: { + adminUserEnabled: acrAdminUserEnabled + } +} + +output loginServer string = acr.properties.loginServer +output adminUsername string = acrAdminUserEnabled ? acr.name : '' +output adminPassword string = acrAdminUserEnabled ? listCredentials(acr.id, acr.apiVersion).passwords[0].value : '' diff --git a/infra/modules/app-service-plan.bicep b/infra/modules/app-service-plan.bicep new file mode 100644 index 000000000..7ce97b585 --- /dev/null +++ b/infra/modules/app-service-plan.bicep @@ -0,0 +1,32 @@ +@description('The name of the App Service Plan') +param name string + +@description('The location of the App Service Plan') +param location string + +@description('The SKU of the App Service Plan') +param sku object = { + capacity: 1 + family: 'B' + name: 'B1' + size: 'B1' + tier: 'Basic' +} + +@description('The kind of the App Service Plan') +param kind string = 'Linux' + +@description('Whether to reserve the App Service Plan') +param reserved bool = true + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = { + name: name + location: location + sku: sku + kind: kind + properties: { + reserved: reserved + } +} + +output id string = appServicePlan.id diff --git a/infra/modules/web-app.bicep b/infra/modules/web-app.bicep new file mode 100644 index 000000000..02fe15792 --- /dev/null +++ b/infra/modules/web-app.bicep @@ -0,0 +1,36 @@ +@description('The name of the web app') +param name string + +@description('The location of the web app') +param location string + +@description('The kind of web app') +param kind string = 'app' + +@description('The ID of the App Service Plan') +param serverFarmResourceId string + +@description('The site configuration') +param siteConfig object + +@description('Application settings') +param appSettingsKeyValuePairs object + +resource webApp 'Microsoft.Web/sites@2022-09-01' = { + name: name + location: location + kind: kind + properties: { + serverFarmId: serverFarmResourceId + siteConfig: siteConfig + } +} + +resource webAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { + parent: webApp + name: 'appsettings' + properties: appSettingsKeyValuePairs +} + +output name string = webApp.name +output defaultHostName string = webApp.properties.defaultHostName diff --git a/requirements.txt b/requirements.txt index d8b131866..d39142a1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -Flask==3.1.0 -gunicorn \ No newline at end of file +Flask==3.0.2 +gunicorn==21.2.0 \ No newline at end of file