diff --git a/.github/actions/CreateDockerImage/action.yaml b/.github/actions/CreateDockerImage/action.yaml new file mode 100644 index 0000000..ce87171 --- /dev/null +++ b/.github/actions/CreateDockerImage/action.yaml @@ -0,0 +1,62 @@ +name: Create Docker Image NSG +description: | + 'Builds a Docker image in the repository' +inputs: + docker_password: + required: true + description: "Docker password" + docker_username: + required: true + description: "Docker username" + docker_owner: + required: true + description: "Docker owner" + docker_args: + description: "Docker build arguments" # e.g. "FOO=bar BAZ=qux" + required: false + default: "" + tag: + description: "v1.0.0" + required: false + default: "latest" + image_name: + description: "Name of the Docker image" + required: true + source_path: + description: "Path to the Dockerfile" + required: true + +runs: + using: "composite" + steps: + - name: Build args function + shell: pwsh + id: build_args + run: | + echo "Building args for docker build" + $input_args = '${{ inputs.docker_args }}' # e.g. "FOO=bar BAZ=qux" + $arr_ = @() + foreach ($input_arg in $input_args -split '\s+') { + if (-not [string]::IsNullOrWhiteSpace($input_arg)) { + $arr_ += "--build-arg $input_arg" + } + } + $joinedArgs = $arr_ -join ' ' # single space-separated string + echo "args=$joinedArgs" >> $env:GITHUB_OUTPUT + + - name: Build docker images and push it to GitHub Container Registry + shell: bash + run: | + echo "Log in to GitHub Container Registry" + echo "${{ inputs.docker_password }}" | docker login ghcr.io -u "${{ inputs.docker_username }}" --password-stdin + REPO_OWNER_LOWER=$(echo "${{ inputs.docker_owner }}" | tr '[:upper:]' '[:lower:]') + + echo "Build ${{ inputs.image_name }} docker image" + docker build \ + ${{ steps.build_args.outputs.args }} \ + -t ghcr.io/$REPO_OWNER_LOWER/${{ inputs.image_name }}:${{ inputs.tag }} \ + "${{ inputs.source_path }}" + + echo "Docker image built successfully" + echo "Push ${{ inputs.image_name }} docker image to GitHub Container Registry" + docker push ghcr.io/$REPO_OWNER_LOWER/${{ inputs.image_name }}:${{ inputs.tag }} diff --git a/.github/actions/CreateNSGRule/action.yaml b/.github/actions/CreateNSGRule/action.yaml new file mode 100644 index 0000000..64f0c47 --- /dev/null +++ b/.github/actions/CreateNSGRule/action.yaml @@ -0,0 +1,89 @@ +name: Create NSG Rule + +inputs: + AZURE_RESOURCE_GROUP: + description: "Azure Resource Group Name" + required: true + type: string + AZURE_NSG_NAME: + description: "Azure NSG Name" + required: true + type: string + AZURE_LOCATION: + description: "Azure Location" + required: true + type: string + AZURE_VNET_NAME: + description: "Azure VNet Name" + required: true + type: string + AZURE_SUBNET_NAME: + description: "Azure Subnet Name" + required: true + type: string + NSG_RULE_NAME: + description: "Azure NSG Rule Name" + required: false + type: string + default: "AllowTraffic" + ALLOWED_PORTS: + description: "Comma-separated list of allowed ports" + required: false + type: string + default: "" + +runs: + using: composite + steps: + - name: Create ${{ inputs.AZURE_NSG_NAME }} NSG Rule Port + shell: bash + run: | + echo "Checking if '${{ inputs.AZURE_NSG_NAME }}' NSG for VNet exists..." + if az network nsg show --name ${{ inputs.AZURE_NSG_NAME }} --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }}; then + echo "NSG already exists." + else + echo "Creating NSG for VNet..." + az network nsg create \ + --name ${{ inputs.AZURE_NSG_NAME }} \ + --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} \ + --location ${{ inputs.AZURE_LOCATION }} + fi + + PORTS=$(echo ${{ inputs.ALLOWED_PORTS }} | tr ',' ' ') + PRIO=1000 + for PORT in $PORTS; do + echo "Checking if NSG rule for port $PORT exists..." + if az network nsg rule show --nsg-name ${{ inputs.AZURE_NSG_NAME }} --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} --name "${{ inputs.NSG_RULE_NAME}}-$PORT-pr-$PRIO"; then + echo "NSG rule for port $PORT already exists." + else + echo "Creating NSG rule for port $PORT..." + az network nsg rule create \ + --nsg-name ${{ inputs.AZURE_NSG_NAME }} \ + --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} \ + --name ${{ inputs.NSG_RULE_NAME}}-$PORT-pr-$PRIO \ + --priority $PRIO \ + --direction Inbound \ + --access Allow \ + --protocol Tcp \ + --source-address-prefixes '*' \ + --source-port-ranges '*' \ + --destination-address-prefixes '*' \ + --destination-port-ranges $PORT + fi + PRIO=$((PRIO + 100)) + done + + - name: Assign NSG to Subnet + shell: bash + run: | + echo "Checking if '${{ inputs.AZURE_NSG_NAME }}' NSG is linked to subnet..." + SUBNET_ID=$(az network vnet subnet show --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} --vnet-name ${{ inputs.AZURE_VNET_NAME }} --name ${{ inputs.AZURE_SUBNET_NAME }} --query id --output tsv) + if az network vnet subnet show --ids $SUBNET_ID --query networkSecurityGroup.id --output tsv | grep -q ${{ inputs.AZURE_NSG_NAME }}; then + echo "NSG is already linked to the subnet." + else + echo "Linking NSG to subnet..." + az network vnet subnet update \ + --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} \ + --ids $SUBNET_ID \ + --network-security-group ${{ inputs.AZURE_NSG_NAME }} + fi diff --git a/.github/actions/CreateWebAPP/action.yaml b/.github/actions/CreateWebAPP/action.yaml new file mode 100644 index 0000000..3d496e6 --- /dev/null +++ b/.github/actions/CreateWebAPP/action.yaml @@ -0,0 +1,82 @@ +name: Create WebApp + +inputs: + WEBAPP_NAME: + description: "Name of the web app" + required: true + type: string + AZURE_APP_SERVICE_PLAN_NAME: + description: "Azure App Service Plan name" + required: true + type: string + AZURE_RESOURCE_GROUP: + description: "Azure Resource Group Name" + required: true + type: string + WEBAPP_VNET_NAME: + description: "Azure Virtual Network Name" + required: true + type: string + WEBAPP_SUBNET: + description: "Azure Subnet Name for the web app" + required: true + type: string + CONTAINER_IMAGE_NAME: + description: "Container image name in GitHub Container Registry" + required: true + type: string + WEBAPP_SETTINGS: + description: "Environment variables for the web app in key=value format(space separated)" + required: false + type: string + default: "" + ENABLE_APP_IDENTITY: + description: Enable Managed Identity for the web app + required: false + type: boolean + default: false + +runs: + using: composite + steps: + - name: Create WebApp + shell: pwsh + run: | + $CONTAINER_IMAGE_NAME="${{ inputs.CONTAINER_IMAGE_NAME }}".tolower() + echo "Creating ${{ inputs.WEBAPP_NAME }} Web App..." + az webapp create ` + --name ${{ inputs.WEBAPP_NAME }} ` + --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} ` + --plan ${{ inputs.AZURE_APP_SERVICE_PLAN_NAME }} ` + --container-registry-url ghcr.io ` + --container-image-name $CONTAINER_IMAGE_NAME + + if ("${{ inputs.ENABLE_APP_IDENTITY }}" -eq "true") { + echo "Enable Managed Identity for ${{ inputs.WEBAPP_NAME }} web app" + az webapp identity assign ` + --name ${{ inputs.WEBAPP_NAME }} ` + --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} + } + + if ('${{ inputs.WEBAPP_SETTINGS }}' -match '^(\w+="[^"]+" ?)+$') { + echo "Set environment variables for ${{ inputs.WEBAPP_NAME }}" + az webapp config appsettings set ` + --name ${{ inputs.WEBAPP_NAME }} ` + --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} ` + --settings ${{ inputs.WEBAPP_SETTINGS }} + } else { + echo "No environment variables to set." + } + + echo "Enable logging for ${{ inputs.WEBAPP_NAME }} web app" + az webapp log config ` + --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} ` + --name ${{ inputs.WEBAPP_NAME }} ` + --docker-container-logging filesystem + + echo "VNet integration for ${{ inputs.WEBAPP_NAME }} web app" + az webapp vnet-integration add ` + --name ${{ inputs.WEBAPP_NAME }} ` + --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} ` + --vnet "${{ inputs.WEBAPP_VNET_NAME }}" ` + --subnet "${{ inputs.WEBAPP_SUBNET }}" diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index e68c72f..64785da 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -5,7 +5,7 @@ on: push: branches: - main - - create_pipeline + - feature/password_in_keyvault permissions: id-token: write @@ -13,18 +13,97 @@ permissions: packages: write jobs: - create_postgres_server_and_key_vault: + create_core_services: + # Network, KeyVault, PostgreSQL name: Deploy resources for restapi runs-on: ubuntu-latest environment: dev steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Login to Azure uses: azure/login@v2 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Create VNet and Subnets + run: | + echo "Checking if VNet exists..." + if az network vnet show --name ${{ vars.AZURE_VNET_NAME }} --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }}; then + echo "VNet already exists." + else + echo "Creating VNet..." + az network vnet create \ + --name ${{ vars.AZURE_VNET_NAME }} \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --location ${{ vars.AZURE_LOCATION }} \ + --address-prefixes 10.0.0.0/16 + fi + address_prefix=0 + + # Define subnet delegation mapping + declare -A subnet_delegations + subnet_delegations["${{ vars.AZURE_SUBNET_BE }}"]="Microsoft.Web/serverFarms" + subnet_delegations["${{ vars.AZURE_SUBNET_FE }}"]="Microsoft.Web/serverFarms" + subnet_delegations["${{ vars.AZURE_SUBNET_DB }}"]="Microsoft.DBforPostgreSQL/flexibleServers" + + for subnet in Default ${{ vars.AZURE_SUBNET_BE }} ${{ vars.AZURE_SUBNET_FE }} ${{ vars.AZURE_SUBNET_DB }} ; do + echo "Creating Subnet for $subnet..." + delegation="${subnet_delegations[$subnet]}" + if [[ -n "$delegation" ]]; then + az network vnet subnet create \ + --name $subnet \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --vnet-name ${{ vars.AZURE_VNET_NAME }} \ + --address-prefixes 10.0.$address_prefix.0/24 \ + --delegations $delegation + else + az network vnet subnet create \ + --name $subnet \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --vnet-name ${{ vars.AZURE_VNET_NAME }} \ + --address-prefixes 10.0.$address_prefix.0/24 + fi + address_prefix=$((address_prefix + 1)) + done + + # Allow 80 and 443 ports fro, + - name: Create FE NSG + uses: ./.github/actions/CreateNSGRule + with: + AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }} + AZURE_NSG_NAME: ${{ vars.AZURE_NSG_NAME_FE }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + NSG_RULE_NAME: "AllowTraffic4Port" + ALLOWED_PORTS: "80,443" + AZURE_VNET_NAME: ${{ vars.AZURE_VNET_NAME }} + AZURE_SUBNET_NAME: ${{ vars.AZURE_SUBNET_FE }} + # No need any specific rules for BE NSG + # Needed functionality covered by default rules + - name: Create BE NSG + uses: ./.github/actions/CreateNSGRule + with: + AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }} + AZURE_NSG_NAME: ${{ vars.AZURE_NSG_NAME_BE }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_VNET_NAME: ${{ vars.AZURE_VNET_NAME }} + AZURE_SUBNET_NAME: ${{ vars.AZURE_SUBNET_BE }} + + # No need any specific rules for BE NSG + # Needed functionality covered by default rules + - name: Create DB NSG + uses: ./.github/actions/CreateNSGRule + with: + AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }} + AZURE_NSG_NAME: ${{ vars.AZURE_NSG_NAME_DB }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_VNET_NAME: ${{ vars.AZURE_VNET_NAME }} + AZURE_SUBNET_NAME: ${{ vars.AZURE_SUBNET_DB }} + - name: Create Key Vault run: | echo "Checking if Key Vault exists..." @@ -37,37 +116,94 @@ jobs: --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ --location ${{ vars.AZURE_LOCATION }} fi - - az role assignment create \ - --assignee ${{ vars.DEV_GROUP_ID }} \ - --role "Key Vault Secrets Officer" \ - --scope "/subscriptions/${{ vars.AZURE_SUBSCRIPTION_ID }}/resourceGroups/${{ secrets.AZURE_RESOURCE_GROUP }}/providers/Microsoft.KeyVault/vaults/${{ vars.AZURE_KEY_VAULT_NAME }}" - az role assignment create \ - --assignee ${{ vars.AZURE_CLIENT_ID }} \ - --role "Key Vault Secrets Officer" \ - --scope "/subscriptions/${{ vars.AZURE_SUBSCRIPTION_ID }}/resourceGroups/${{ secrets.AZURE_RESOURCE_GROUP }}/providers/Microsoft.KeyVault/vaults/${{ vars.AZURE_KEY_VAULT_NAME }}" + KEY_VAULT_ID=$(az keyvault show --name ${{ vars.AZURE_KEY_VAULT_NAME }} --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --query id -o tsv) + + for assignee in ${{ vars.DEV_GROUP_ID }} ${{ vars.AZURE_CLIENT_ID }}; do + existing=$(az role assignment list --scope "$KEY_VAULT_ID" --assignee "$assignee" --role "Key Vault Secrets Officer" --query "[].id" -o tsv) + if [ -z "$existing" ]; then + az role assignment create \ + --assignee "$assignee" \ + --role "Key Vault Secrets Officer" \ + --scope $KEY_VAULT_ID + else + echo "Role assignment already exists, skipping creation." + fi + done + + - name: Create Key Vault Private Endpoint + run: | + SUBNET="Default" + PE="restapi-kv-pe-we-01" + ZONE="privatelink.vaultcore.azure.net" + CONN_NAME="restapi-kv-pe-conn" + + # get resource IDs + KEY_VAULT_ID=$(az keyvault show --name ${{ vars.AZURE_KEY_VAULT_NAME }} --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --query id -o tsv) + VNET_ID=$(az network vnet show -g "${{ secrets.AZURE_RESOURCE_GROUP }}" -n ${{ vars.AZURE_VNET_NAME }} --query id -o tsv) + + # Check if the DNS zone exists + existing_zone=$(az network private-dns zone list --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --query "[?name=='$ZONE']" -o tsv) + + if [ -z "$existing_zone" ]; then + echo "Creating private DNS zone for Key Vault" + az network private-dns zone create --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}" --name $ZONE + az network private-dns link vnet create \ + --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}" \ + --name kv-dns-link \ + --location ${{ vars.AZURE_LOCATION }} \ + --zone-name $ZONE \ + --virtual-network $VNET_ID \ + --registration-enabled false + else + echo "DNS zone $ZONE already exists, skipping creation." + fi + + echo "Creating private endpoint" + az network private-endpoint create \ + --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}" \ + --name $PE \ + --location ${{ vars.AZURE_LOCATION }} \ + --vnet-name ${{ vars.AZURE_VNET_NAME }} --subnet $SUBNET \ + --private-connection-resource-id $KEY_VAULT_ID \ + --group-ids vault --connection-name $CONN_NAME + + echo "Attaching the PE to the DNS zone (auto creates A record)" + PE_ID=$(az network private-endpoint show --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}" --name $PE --query id -o tsv) + az network private-endpoint dns-zone-group create \ + --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}" \ + --endpoint-name $PE \ + --name kv-dns-group \ + --private-dns-zone $ZONE \ + --zone-name $ZONE - name: Create PostgreSQL Server + id: create_postgres_server run: | - IP=$(curl -s https://ifconfig.me) - echo "Runner Public IP: $IP" if az postgres flexible-server show --name "${{ vars.AZURE_POSTGRESQL_SERVER_NAME }}" --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}"; then echo "PostgreSQL server already exists." + echo "Fetching PostgreSQL admin password from Azure Key Vault" + PASSWORD=$(az keyvault secret show --name "AZURE-POSTGRESQL-ADMIN-PASSWORD" --vault-name "${{ vars.AZURE_KEY_VAULT_NAME }}" --query value -o tsv) else + echo "Generate password for database admin user" + PASSWORD=$(openssl rand -base64 16) + echo "Creating PostgreSQL server..." az postgres flexible-server create \ --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}" \ --name "${{ vars.AZURE_POSTGRESQL_SERVER_NAME }}" \ --location "${{ vars.AZURE_LOCATION }}" \ --admin-user "${{ secrets.AZURE_POSTGRESQL_ADMIN_USER }}" \ - --admin-password "${{ secrets.AZURE_POSTGRESQL_ADMIN_PASSWORD }}" \ + --admin-password "$PASSWORD" \ --tier Burstable \ --sku-name Standard_B1ms \ --version 16 \ - --public-access $IP - fi - az keyvault secret set --vault-name "${{ vars.AZURE_KEY_VAULT_NAME }}" --name "AZURE-POSTGRESQL-ADMIN-PASSWORD" --value "${{ secrets.AZURE_POSTGRESQL_ADMIN_PASSWORD }}" + --vnet "${{ vars.AZURE_VNET_NAME }}" \ + --subnet "${{ vars.AZURE_SUBNET_DB }}" \ + --yes + + echo "Save PostgreSQL admin user and password in Azure Key Vault" + az keyvault secret set --vault-name "${{ vars.AZURE_KEY_VAULT_NAME }}" --name "AZURE-POSTGRESQL-ADMIN-PASSWORD" --value "$PASSWORD" az keyvault secret set --vault-name "${{ vars.AZURE_KEY_VAULT_NAME }}" --name "AZURE-POSTGRESQL-ADMIN-USER" --value "${{ secrets.AZURE_POSTGRESQL_ADMIN_USER }}" echo "Create PostgreSQL database" @@ -75,60 +211,67 @@ jobs: --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}" \ --server-name "${{ vars.AZURE_POSTGRESQL_SERVER_NAME }}" \ --database-name tasks_db - - # echo "Allow public access from any Azure service within Azure to this server" - # az postgres flexible-server update \ - # --resource-group "${{ secrets.AZURE_RESOURCE_GROUP }}" \ - # --name "${{ vars.AZURE_POSTGRESQL_SERVER_NAME }}" \ - # --public-network-access Enabled + fi + echo "::add-mask::$PASSWORD" build_docker_image: name: Build docker image and save in Github Container Registry runs-on: ubuntu-latest environment: dev + needs: [create_core_services] env: WEBAPP_BACKEND_NAME: ${{ vars.WEBAPP_BACKEND_NAME }} steps: + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + - name: Checkout code uses: actions/checkout@v2 + - name: Build and push BE Docker image + uses: ./.github/actions/CreateDockerImage + with: + docker_password: ${{ secrets.GITHUB_TOKEN }} + docker_username: ${{ github.actor }} + docker_owner: ${{ github.repository_owner }} + image_name: rest-api-backend + source_path: backend_app + tag: latest + docker_args: | + POSTGRES_PASSWORD="Placeholder" + POSTGRES_HOST="Placeholder" + POSTGRES_USER="Placeholder" - - name: Build docker images and push it to GitHub Container Registry - run: | - echo "Log in to GitHub Container Registry" - echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') - - echo "Build BE docker image" - docker build -t ghcr.io/$REPO_OWNER_LOWER/rest-api-backend:latest backend_app - - echo "Docker image built successfully" - echo "Push BE docker image to GitHub Container Registry" - docker push ghcr.io/$REPO_OWNER_LOWER/rest-api-backend:latest - - VITE_BACKEND_API_URL="https://rest-api-$WEBAPP_BACKEND_NAME.azurewebsites.net" - - echo "Build FE docker image" - docker build \ - --build-arg VITE_BACKEND_API_URL=$VITE_BACKEND_API_URL \ - -t ghcr.io/$REPO_OWNER_LOWER/rest-api-frontend:latest frontend_app - - echo "Docker image built successfully" - echo "Push FE docker image to GitHub Container Registry" - docker push ghcr.io/$REPO_OWNER_LOWER/rest-api-frontend:latest - - create_webapp: + - name: Build and push FE Docker image + uses: ./.github/actions/CreateDockerImage + with: + docker_password: ${{ secrets.GITHUB_TOKEN }} + docker_username: ${{ github.actor }} + docker_owner: ${{ github.repository_owner }} + image_name: rest-api-frontend + source_path: frontend_app + tag: latest + docker_args: | + VITE_BACKEND_API_URL="https://rest-api-$WEBAPP_BACKEND_NAME.azurewebsites.net" + + deploy_web_apps: name: Deploy Web Apps runs-on: ubuntu-latest environment: dev - needs: [create_postgres_server_and_key_vault, build_docker_image] - + needs: [build_docker_image] steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Login to Azure uses: azure/login@v2 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + - name: Create App Service Plan run: | echo "Checking if App Service Plan exists..." @@ -143,55 +286,43 @@ jobs: --sku B2 \ --is-linux fi + - name: Create backend_app + uses: ./.github/actions/CreateWebAPP + with: + WEBAPP_NAME: "rest-api-${{ vars.WEBAPP_BACKEND_NAME }}" + AZURE_APP_SERVICE_PLAN_NAME: ${{ vars.AZURE_APP_SERVICE_PLAN_NAME }} + AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }} + WEBAPP_VNET_NAME: ${{ vars.AZURE_VNET_NAME }} + WEBAPP_SUBNET: ${{ vars.AZURE_SUBNET_BE }} + CONTAINER_IMAGE_NAME: "${{ github.repository_owner }}/rest-api-backend:latest" + ENABLE_APP_IDENTITY: true + WEBAPP_SETTINGS: 'POSTGRES_HOST="${{ vars.AZURE_POSTGRESQL_SERVER_NAME }}.postgres.database.azure.com" POSTGRES_USER="${{ secrets.AZURE_POSTGRESQL_ADMIN_USER }}" POSTGRES_PASSWORD="@Microsoft.KeyVault(SecretUri=https://${{ vars.AZURE_KEY_VAULT_NAME }}.vault.azure.net/secrets/AZURE-POSTGRESQL-ADMIN-PASSWORD)"' + + - name: Grant permissions to the BackendApp managed identity run: | - BACKEND_WEBAPP_NAME="rest-api-${{ vars.WEBAPP_BACKEND_NAME }}" - PG_SERVER_NAME="${{ vars.AZURE_POSTGRESQL_SERVER_NAME }}.postgres.database.azure.com" + KEY_VAULT_ID=$(az keyvault show --name ${{ vars.AZURE_KEY_VAULT_NAME }} --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --query id -o tsv) + APP_PRINCIPAL_ID=$(az webapp identity show -g "${{ secrets.AZURE_RESOURCE_GROUP }}" --name "rest-api-${{ vars.WEBAPP_BACKEND_NAME }}" --query principalId -o tsv) - REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') - echo "Creating Web App..." - az webapp create \ - --name $BACKEND_WEBAPP_NAME \ - --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ - --plan ${{ vars.AZURE_APP_SERVICE_PLAN_NAME }} \ - --container-registry-url ghcr.io \ - --container-image-name $REPO_OWNER_LOWER/rest-api-backend:latest - - echo "Set environment variables for backend web app" - az webapp config appsettings set \ - --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ - --name $BACKEND_WEBAPP_NAME \ - --settings \ - POSTGRES_HOST="$PG_SERVER_NAME" \ - POSTGRES_USER="${{ secrets.AZURE_POSTGRESQL_ADMIN_USER }}" \ - POSTGRES_PASSWORD="${{ secrets.AZURE_POSTGRESQL_ADMIN_PASSWORD }}" - - echo "Enable logging for backend web app" - az webapp log config \ - --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ - --name $BACKEND_WEBAPP_NAME \ - --docker-container-logging filesystem + existing=$(az role assignment list --scope "$KEY_VAULT_ID" --assignee "$APP_PRINCIPAL_ID" --role "Key Vault Secrets User" --query "[].id" -o tsv) + if [ -z "$existing" ]; then + echo "Granting Key Vault access to Backend APP ($APP_PRINCIPAL_ID)" + az role assignment create \ + --assignee-object-id "$APP_PRINCIPAL_ID" \ + --assignee-principal-type ServicePrincipal \ + --role "Key Vault Secrets User" \ + --scope "$KEY_VAULT_ID" + else + echo "Backend APP ($APP_PRINCIPAL_ID) already has Key Vault access." + fi - name: Create frontend_app - run: | - FRONTEND_WEBAPP_NAME="rest-api-${{ vars.WEBAPP_FRONTEND_NAME }}" - REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') - echo "Creating Web App..." - az webapp create \ - --name "$FRONTEND_WEBAPP_NAME" \ - --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ - --plan ${{ vars.AZURE_APP_SERVICE_PLAN_NAME }} \ - --container-registry-url ghcr.io \ - --container-image-name $REPO_OWNER_LOWER/rest-api-frontend:latest - - echo "Configuring frontend_app settings..." - az webapp config appsettings set \ - --name $FRONTEND_WEBAPP_NAME \ - --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ - --settings "VITE_BACKEND_API_URL=https://rest-api-${{ vars.WEBAPP_BACKEND_NAME }}.azurewebsites.net" - - echo "Enable logging for frontend web app" - az webapp log config \ - --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ - --name $FRONTEND_WEBAPP_NAME \ - --docker-container-logging filesystem + uses: ./.github/actions/CreateWebAPP + with: + WEBAPP_NAME: "rest-api-${{ vars.WEBAPP_FRONTEND_NAME }}" + AZURE_APP_SERVICE_PLAN_NAME: ${{ vars.AZURE_APP_SERVICE_PLAN_NAME }} + AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }} + WEBAPP_VNET_NAME: ${{ vars.AZURE_VNET_NAME }} + WEBAPP_SUBNET: ${{ vars.AZURE_SUBNET_FE }} + CONTAINER_IMAGE_NAME: "${{ github.repository_owner }}/rest-api-frontend:latest" + WEBAPP_SETTINGS: 'VITE_BACKEND_API_URL="https://rest-api-${{ vars.WEBAPP_BACKEND_NAME }}.azurewebsites.net"' diff --git a/backend_app/Dockerfile b/backend_app/Dockerfile index 246dc74..02f69ea 100644 --- a/backend_app/Dockerfile +++ b/backend_app/Dockerfile @@ -4,6 +4,9 @@ FROM python:3.11-slim # Set the working directory inside the container to /app WORKDIR /app +ARG POSTGRES_PASSWORD +ENV POSTGRES_PASSWORD=$POSTGRES_PASSWORD + # Copy the requirements.txt file from the host to the container COPY requirements.txt .