diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..0b1e1e7ef --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml new file mode 100644 index 000000000..eb4a18b68 --- /dev/null +++ b/.github/workflows/container.yml @@ -0,0 +1,64 @@ +name: Deploy Container App to Azure + +on: + push: + branches: + - main + +env: + REGISTRY_LOGIN_SERVER: annacr.azurecr.io + IMAGE_BASE_NAME: annaimage + WEBAPP_NAME: annawebapp + +jobs: + build-and-push: + runs-on: ubuntu-latest + outputs: + image-version: ${{ steps.image-version.outputs.version }} + steps: + - name: Checkout code + uses: actions/checkout@main + + - name: Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + + + - name: Login to ACR + uses: azure/docker-login@v1 + with: + login-server: ${{ env.REGISTRY_LOGIN_SERVER }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Set image version + id: image-version + run: echo "version=${GITHUB_REF#refs/heads/}-$(date +'%Y.%m.%d.%H.%M')" >> $GITHUB_OUTPUT + + + - name: Build and Push Docker Image + run: | + docker build . -t ${{ env.REGISTRY_LOGIN_SERVER }}/${{env.IMAGE_BASE_NAME}}:${{ steps.image-version.outputs.version }} + docker build . -t ${{ env.REGISTRY_LOGIN_SERVER }}/${{env.IMAGE_BASE_NAME}}:${{ github.ref_name }}-latest + docker push ${{ env.REGISTRY_LOGIN_SERVER }}/${{env.IMAGE_BASE_NAME}}:${{ steps.image-version.outputs.version }} + docker push ${{ env.REGISTRY_LOGIN_SERVER }}/${{env.IMAGE_BASE_NAME}}:${{ github.ref_name }}-latest + + deploy: + runs-on: ubuntu-latest + needs: build-and-push + steps: + - name: Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Deploy Docker Image to Azure Web App + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ env.WEBAPP_NAME }} + images: | + ${{ env.REGISTRY_LOGIN_SERVER }}/${{ env.IMAGE_BASE_NAME }}:${{ needs.build-and-push.outputs.image-version }} + + diff --git a/.github/workflows/deployinfra.yml b/.github/workflows/deployinfra.yml new file mode 100644 index 000000000..272bd52ff --- /dev/null +++ b/.github/workflows/deployinfra.yml @@ -0,0 +1,42 @@ +name: Deploy to Development + +on: + workflow_dispatch: + push: + branches: + - main + +env: + RESOURCE_GROUP_DEV: BCSAI2024-DEVOPS-STUDENTS-A-DEV + SUBSCRIPTION_ID_DEV: e0b9cada-61bc-4b5a-bd7a-52c606726b3b + + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run Bicep linter + run: az bicep build --file ./main.bicep + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Development deployment' + + steps: + - uses: actions/checkout@v2 + - uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + - name: deploy + uses: azure/arm-deploy@v2 + with: + subscriptionId: ${{ env.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ env.RESOURCE_GROUP_DEV }} + template: ./main.bicep + parameters: > + ./parameters.bicepparam + + diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..fcc1f8f5c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,40 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Flask", + "type": "debugpy", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app.py", + "FLASK_DEBUG": "1" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "jinja": true, + "autoStartBrowser": false + }, + { + "name": "Docker: Python - Flask", + "type": "docker", + "request": "launch", + "preLaunchTask": "docker-run: debug", + "python": { + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app" + } + ], + "projectType": "flask" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..d0f1f161f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,40 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "docker-build", + "label": "docker-build", + "platform": "python", + "dockerBuild": { + "tag": "msdocspythonflaskwebappquickstart1:latest", + "dockerfile": "${workspaceFolder}/Dockerfile", + "context": "${workspaceFolder}", + "pull": true + } + }, + { + "type": "docker-run", + "label": "docker-run: debug", + "dependsOn": [ + "docker-build" + ], + "dockerRun": { + "env": { + "FLASK_APP": "app.py" + } + }, + "python": { + "args": [ + "run", + "--no-debugger", + "--no-reload", + "--host", + "0.0.0.0", + "--port", + "5000" + ], + "module": "flask" + } + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..2e77bf95e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# For more information, please refer to https://aka.ms/vscode-docker-python +FROM python:3-slim + +EXPOSE 5000 + +# Keeps Python from generating .pyc files in the container +ENV PYTHONDONTWRITEBYTECODE=1 + +# Turns off buffering for easier container logging +ENV PYTHONUNBUFFERED=1 + +# Install pip requirements +COPY requirements.txt . +RUN python -m pip install -r requirements.txt + +WORKDIR /app +COPY . /app + +# Creates a non-root user with an explicit UID and adds permission to access the /app folder +# For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers +RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app +USER appuser + +# During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] + \ No newline at end of file diff --git a/guniicorn.conf.py b/guniicorn.conf.py new file mode 100644 index 000000000..fea6a15e4 --- /dev/null +++ b/guniicorn.conf.py @@ -0,0 +1,14 @@ +# Gunicorn configuration file +import multiprocessing + +max_requests = 1000 +max_requests_jitter = 50 + +log_file = "-" + +bind = "0.0.0.0:50505" + +workers = (multiprocessing.cpu_count() * 2) + 1 +threads = workers + +timeout = 120 \ No newline at end of file diff --git a/main.bicep b/main.bicep new file mode 100644 index 000000000..e1aa67c78 --- /dev/null +++ b/main.bicep @@ -0,0 +1,77 @@ +@description('Name of the Azure Container Registry') +param containerRegistryName string + +@description('Location of resources') +param location string + +@description('Image name for the container') +param containerRegistryImageName string + +@description('Image version for the container') +param containerRegistryImageVersion string + +@description('Name of the App Service Plan') +param appServicePlanName string + +@description('Name of the Web App') +param webAppName string + +@description('The Key Vault name') +param keyVaultName string +@description('The Key Vault SKU') +param keyVaultSku string +param enableSoftDelete bool +@sys.description('The role assignments for the Key Vault') +param keyVaultRoleAssignments array +var adminPasswordSecretName = 'adminPasswordSecretName' +var adminUsernameSecretName = 'adminUsernameSecretName' + +module keyVault 'modules/kv.bicep' = { + name: keyVaultName + params: { + name: keyVaultName + location: location + sku: keyVaultSku + roleAssignments: keyVaultRoleAssignments + enableVaultForDeployment: true + enableSoftDelete: enableSoftDelete + } +} + +module containerRegistryModule './modules/cr.bicep' = { + name: containerRegistryName + params: { + keyVaultResourceId: keyVault.outputs.resourceId + keyVaultSecreNameAdminUsername: adminUsernameSecretName + keyVaultSecreNameAdminPassword: adminPasswordSecretName + containerRegistryName: containerRegistryName + location: location + + } +} + +module appServicePlanModule './modules/apsp.bicep' = { + name: appServicePlanName + params: { + appServicePlanName: appServicePlanName + location: location + } +} + +resource keyVaultReference 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: keyVaultName + } +module webAppModule './modules/web.bicep' = { + name: webAppName + params: { + webAppName: webAppName + location: location + appServicePlanId: appServicePlanModule.outputs.id + containerRegistryName: containerRegistryName + dockerRegistryImageName: containerRegistryImageName + dockerRegistryImageVersion: containerRegistryImageVersion + dockerRegistryServerUserName: keyVaultReference.getSecret(adminUsernameSecretName) + dockerRegistryServerPassword: keyVaultReference.getSecret(adminPasswordSecretName) + + } +} diff --git a/modules/apsp.bicep b/modules/apsp.bicep new file mode 100644 index 000000000..7d3c8d2e4 --- /dev/null +++ b/modules/apsp.bicep @@ -0,0 +1,20 @@ +param appServicePlanName string +param location string = resourceGroup().location + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { + name: appServicePlanName + location: location + sku: { + capacity: 1 + family: 'B' + name: 'B1' + size: 'B1' + tier: 'Basic' + } + kind: 'Linux' + properties: { + reserved: true + } +} + +output id string = appServicePlan.id diff --git a/modules/cr.bicep b/modules/cr.bicep new file mode 100644 index 000000000..df34c969b --- /dev/null +++ b/modules/cr.bicep @@ -0,0 +1,48 @@ +param containerRegistryName string +param location string = resourceGroup().location +param keyVaultResourceId string +#disable-next-line secure-secrets-in-params +param keyVaultSecreNameAdminUsername string +#disable-next-line secure-secrets-in-params +param keyVaultSecreNameAdminPassword string + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { + name: containerRegistryName + location: location + sku: { + name: 'Basic' + } + properties: { + adminUserEnabled: true + } +} + +output id string = containerRegistry.id +output loginServer string = containerRegistry.properties.loginServer + +resource adminCredentialsKeyVault 'Microsoft.KeyVault/vaults@2021-10-01' existing = if (!empty(keyVaultResourceId)) { + name: last(split((!empty(keyVaultResourceId) ? keyVaultResourceId : 'dummyVault'), '/'))! +} + +// create a secret to store the container registry admin username +resource secretAdminUserName 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = if (!empty(keyVaultSecreNameAdminUsername)) { + name: !empty(keyVaultSecreNameAdminUsername) ? keyVaultSecreNameAdminUsername : 'dummySecret' + parent: adminCredentialsKeyVault + properties: { + value: containerRegistry.listCredentials().username +} +} +// create a secret to store the container registry admin password 0 +resource secretAdminUserPassword0 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = if (!empty(keyVaultSecreNameAdminPassword)) { + name: !empty(keyVaultSecreNameAdminPassword) ? keyVaultSecreNameAdminPassword : 'dummySecret' + parent: adminCredentialsKeyVault + properties: { + value: containerRegistry.listCredentials().passwords[0].value +} +} +// #disable-next-line outputs-should-not-contain-secrets +// var credentials = containerRegistry.listCredentials() +// #disable-next-line outputs-should-not-contain-secrets +// output adminUsername string = credentials.username +// #disable-next-line outputs-should-not-contain-secrets +// output adminPassword string = credentials.passwords[0].value diff --git a/modules/kv.bicep b/modules/kv.bicep new file mode 100644 index 000000000..61ea73c6b --- /dev/null +++ b/modules/kv.bicep @@ -0,0 +1,123 @@ +param name string +@description('Enable RBAC authorization for Key Vault (default: true).') +param enableRbacAuthorization bool = true + +@description('Optional. Location for all resources.') +param location string = resourceGroup().location + +@description('Specifies if the vault is enabled for deployment by script or compute.') +param enableVaultForDeployment bool = true + +@description('Specifies if the vault is enabled for a template deployment.') +param enableVaultForTemplateDeployment bool = true + +@description('Enable Key Vault\'s soft delete feature.') +param enableSoftDelete bool + +@description('Specifies the SKU for the vault.') +param sku string +param roleAssignments array = [] + +var builtInRoleNames = { + Contributor: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b24988ac-6180-42a0-ab88-20f7382dd24c' + ) + 'Key Vault Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '00482a5a-887f-4fb3-b363-3b7fe8e74483' + ) + 'Key Vault Certificates Officer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a4417e6f-fecd-4de8-b567-7b0420556985' + ) + 'Key Vault Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f25e0fa2-a7c8-4377-a976-54943a77a395' + ) + 'Key Vault Crypto Officer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '14b46e9e-c2b7-41b4-b07b-48a6ebf60603' + ) + 'Key Vault Crypto Service Encryption User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'e147488a-f6f5-4113-8e2d-b22465e65bf6' + ) + 'Key Vault Crypto User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '12338af0-0e69-4776-bea7-57ae8d297424' + ) + 'Key Vault Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '21090545-7ca7-4776-b22c-e363652d74d2' + ) + 'Key Vault Secrets Officer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7' + ) + 'Key Vault Secrets User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '4633458b-17de-408a-b874-0445c86b69e6' + ) + Owner: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' + ) + Reader: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'acdd72a7-3385-48ef-bd42-f606fba81ae7' + ) + 'Role Based Access Control Administrator (Preview)': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f58310d9-a9f6-439a-9e8d-f62e7b41a168' + ) + 'User Access Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' + ) +} + + +// Key Vault Resource... +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: name + location: location + properties: { + enabledForDeployment: enableVaultForDeployment + enabledForTemplateDeployment: enableVaultForTemplateDeployment + enableSoftDelete: enableSoftDelete + enableRbacAuthorization: enableRbacAuthorization + + sku: { + name: sku + family: 'A' + } + accessPolicies: [ ] + tenantId: subscription().tenantId + } +} + +resource keyVault_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ +for (roleAssignment, index) in (roleAssignments ?? []): { + name: guid(keyVault.id, roleAssignment.principalId,roleAssignment.roleDefinitionIdOrName) + properties: { + roleDefinitionId:builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? roleAssignment.roleDefinitionIdOrName + principalId: roleAssignment.principalId + description: roleAssignment.?description + principalType: roleAssignment.?principalType + condition: roleAssignment.?condition + conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set + delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId + } + scope: keyVault +} +] + + + +// Outputs +@description('The resource ID of the key vault.') +output resourceId string = keyVault.id + +@description('The URI of the key vault.') +output keyVaultUri string = keyVault.properties.vaultUri diff --git a/modules/web.bicep b/modules/web.bicep new file mode 100644 index 000000000..5bb68bd71 --- /dev/null +++ b/modules/web.bicep @@ -0,0 +1,33 @@ +param webAppName string +param location string = resourceGroup().location +param appServicePlanId string +param containerRegistryName string +@secure() +param dockerRegistryServerUserName string +@secure() +param dockerRegistryServerPassword string +param dockerRegistryImageName string +param dockerRegistryImageVersion string = 'latest' +param appSettings array = [] +var dockerAppSettings = [ + { name: 'DOCKER_REGISTRY_SERVER_URL', value: 'https://${containerRegistryName}.azurecr.io' } + { name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: dockerRegistryServerUserName } + { name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: dockerRegistryServerPassword } + { name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', value: 'false'} +] + + +resource webApp 'Microsoft.Web/sites@2022-03-01' = { + name: webAppName + location: location + kind: 'app' + properties: { + serverFarmId: appServicePlanId + siteConfig: { + linuxFxVersion: 'DOCKER|${containerRegistryName}.azurecr.io/${dockerRegistryImageName}:${dockerRegistryImageVersion}' + appCommandLine: '' + appSettings: union(appSettings, dockerAppSettings) + + } + } +} diff --git a/parameters.bicepparam b/parameters.bicepparam new file mode 100644 index 000000000..6421f6ae2 --- /dev/null +++ b/parameters.bicepparam @@ -0,0 +1,27 @@ +using './main.bicep' + +param location = 'North Europe' +param containerRegistryName = 'annacr' +param containerRegistryImageName = 'annaimage' +param containerRegistryImageVersion = 'main-latest' +param appServicePlanName = 'annaasp' +param webAppName = 'annawebapp' +param keyVaultName = 'annakv' +param keyVaultSku = 'standard' +param enableSoftDelete = true +param keyVaultRoleAssignments = [ + { + principalId: '25d8d697-c4a2-479f-96e0-15593a830ae5' // BCSAI2024-DEVOPS-STUDENTS-A-SP + roleDefinitionIdOrName: 'Key Vault Secrets User' + principalType: 'ServicePrincipal' + } + { + principalId: 'a03130df-486f-46ea-9d5c-70522fe056de' // BCSAI2024-DEVOPS-STUDENTS-A + roleDefinitionIdOrName: 'Key Vault Administrator' + principalType: 'Group' + } +] + + + + \ No newline at end of file