Deploying Drupal Applications on App Service Linux with App Cache
This blog will show you have to deploy your Drupal Site to an Linux PHP App Service with app cache for better preformance.
Overview
This blog will show you have to deploy your Drupal Site to an Linux PHP App Service.
Since Durpal is a CMS customers following a standard deployment pattern might run into the certain pitfalls such as slow preformance under load and long deployment times.
This is due to the underlying app service architecture, Azure App Service uses a networked mapped storage account to persist files and sync them between instances. However CMS frameworks rely heavily on I/O operations for every request, which will result in slow preformance on most sites.
We will cover the following topics in this blog.
- Quicker Preformance by enabling the app setting WEBSITES_ENABLE_APP_CACHE.
- References on how to create your modify the Drupal Nginx configuration.
- Quicker deployment times using either Azure DevOps or Github Actions Build Pipelines.
Prerequisites
This guide assumes that you already have a working Drupal Site synced to source control.
For this demo we are using the pre-made unami, food blog provided by Drupal- Drupal Quickstart
Will be using the Drupal ‘Unami’ demo connected to an Azure Database for MySQL
Here is my repo that containers the code for this demo and all the sample config files: kedsouza - Umami-Demo
Enabling App Cache - [Read Only]
For ease of instructions we will first proceed with enabling the app cache to improve preformance and deployment times.
While enabling app cache will make your Drupal App Perform faster this will not allow you write content to the app service files system. Meaning if your team is making change / adding content these changes will not be presisted. You will lose content when the app service preforms a restart operation.
We will explore possiblities on how to presit Drupal Content in subsquence sections in this blog. However we are seperating these steps because some organization choose not to add /edit content directly to their production durpal site. They instead devolop / test on a dev site and push the changes to production. In this sceneario making Drupal ‘Read Only’ can work and can aid in simplicity.
- Enable the app setting WEBSITES_ENABLE_APP_CACHE.
WEBSITES_ENABLE_APP_CACHE=true
This setting will remove the networked mapped storage from the app service and create a docker volume to host your code base.
More information here: App Cache
Once you add this setting you may see the standard Azure hosting start page being returned by your app service. This is because app cache needs a new deployment once enabled.
-
Add nginx configuration to your repo. The nginx configuration in the default linux app service php image needs to be modified in order to serve Drupal content correctly.
We need to create two files, the custom nginx configuration file and a script to copy over the file on container startup.
We are creating the nginx configuration based on this example from nginx: Drupal - Nginx
The below file is a sample of our standard nginx configuration, which can be find by using the ssh feature and naviagting to
/etc/nginx/sites-available/default
and the above Drupal configuration.For this example I named this configuraiton nginx-default
server { listen 8080; listen [::]:8080; root /home/site/wwwroot/web; index index.php index.html index.htm; server_name example.com www.example.com; port_in_redirect off; location = /favicon.ico { log_not_found off; access_log off; } location = /robots.txt { allow all; log_not_found off; access_log off; } # Very rarely should these ever be accessed outside of your lan location ~* \.(txt|log)$ { allow 192.168.0.0/16; deny all; } location ~ \..*/.*\.php$ { return 403; } location ~ ^/sites/.*/private/ { return 403; } # Block access to scripts in site files directory location ~ ^/sites/[^/]+/files/.*\.php$ { deny all; } # Allow "Well-Known URIs" as per RFC 5785 location ~* ^/.well-known/ { allow all; } # Block access to "hidden" files and directories whose names begin with a # period. This includes directories used by version control systems such # as Subversion or Git to store control files. location ~ (^|/)\. { return 403; } location / { # try_files $uri @rewrite; # For Drupal <= 6 try_files $uri /index.php?$query_string; # For Drupal >= 7 } location @rewrite { #rewrite ^/(.*)$ /index.php?q=$1; # For Drupal <= 6 rewrite ^ /index.php; # For Drupal >= 7 } # Don't allow direct access to PHP files in the vendor directory. location ~ /vendor/.*\.php$ { deny all; return 404; } # Protect files and directories from prying eyes. location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ { deny all; return 404; } # Add locations of phpmyadmin here. location ~* [^/]\.php(/|$) { fastcgi_split_path_info ^(.+?\.[Pp][Hh][Pp])(|/.*)$; fastcgi_pass 127.0.0.1:9000; include fastcgi_params; fastcgi_param HTTP_PROXY ""; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param QUERY_STRING $query_string; fastcgi_intercept_errors on; fastcgi_connect_timeout 300; fastcgi_send_timeout 3600; fastcgi_read_timeout 3600; fastcgi_buffer_size 128k; fastcgi_buffers 4 256k; fastcgi_busy_buffers_size 256k; fastcgi_temp_file_write_size 256k; } }
The startup script will copy this configuration to the nginx config directory on the container. You can find other blogs about this topic here: NGINX Rewrite Rules for Azure App Service Linux PHP 8.x
#!/bin/bash cp /home/site/wwwroot/nginx-default /etc/nginx/sites-enabled/default service nginx reload
Reference this startup script within the app service startup command.
- Retry a deployment, if you are using the Oryx build system this. Depending on your Drupal Site this might take a while 15 - 20 minutes, however the deployment should still work. This is because most Drupal Repos contain a lot of files in which the app service networked mapped file system takes a while to transfer.
Afte a deployment a read only version of your Drupal site should be working as expected and preforming a magtidude faster.
Improve Deployment Times.
Due to the number of files and content a Standard Drupal Repo, deploying can sometimes be slow, for this reason if your team notices longer then desired deployment times please disable the oryx deployment and follow the below steps.
To disable this please add the below app setting before choosing one of the below deployment methods.
SCM_DO_BUILD_DURING_DEPLOYMENT = false
Azure Devops
Create your default Azure DevOps Pipeline the pregenerated App Service Linux PHP Template should work.
Below is my sample pipeline, your should look similar.
# PHP as Linux Web App on Azure
# Build, package and deploy your PHP project to Azure Linux Web App.
# Add steps that run tests and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/php
trigger:
- main
variables:
# Azure Resource Manager connection created during pipeline creation
azureSubscription: '{Your Subscription ID}'
# Web app name
webAppName: '{Your App Service Name}'
# Agent VM image name
vmImageName: 'ubuntu-latest'
# Environment name
environmentName: '{Your App Service Name}'
# Root folder under which your composer.json file is available.
rootFolder: $(System.DefaultWorkingDirectory)
stages:
- stage: Build
displayName: Build stage
variables:
phpVersion: '8.2'
jobs:
- job: BuildJob
pool:
vmImage: $(vmImageName)
steps:
- script: |
sudo update-alternatives --set php /usr/bin/php$(phpVersion)
sudo update-alternatives --set phar /usr/bin/phar$(phpVersion)
sudo update-alternatives --set phpdbg /usr/bin/phpdbg$(phpVersion)
sudo update-alternatives --set php-cgi /usr/bin/php-cgi$(phpVersion)
sudo update-alternatives --set phar.phar /usr/bin/phar.phar$(phpVersion)
php -version
workingDirectory: $(rootFolder)
displayName: 'Use PHP version $(phpVersion)'
- script: composer install --no-interaction --prefer-dist
workingDirectory: $(rootFolder)
displayName: 'Composer install'
- task: ArchiveFiles@2
displayName: 'Archive files'
inputs:
rootFolderOrFile: '$(rootFolder)'
includeRootFolder: false
archiveType: zip
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
replaceExistingArchive: true
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
displayName: 'Upload package'
artifact: drop
- stage: Deploy
displayName: 'Deploy Web App'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeploymentJob
pool:
vmImage: $(vmImageName)
environment: $(environmentName)
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy Azure Web App '
inputs:
azureSubscription: $(azureSubscription)
appName: $(webAppName)
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
Github Actions
With Github Actions you can base workflow file off the pregenerated template from the app service deployment center.
However you may notice slow transfers between the build / deploy jobs.
We had to tar the artifacts up before transferring between these tasks, below is my Github Actions Workflow file, yours should be similar.
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions
name: Build and deploy PHP app to Azure Web App - kedsouza-php
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Check if composer.json exists
id: check_files
uses: andstor/file-existence-action@v1
with:
files: 'composer.json'
- name: Run composer install if composer.json exists
if: steps.check_files.outputs.files_exists == 'true'
run: composer validate --no-check-publish && composer install --prefer-dist --no-progress
- name: Tar Artifacts to increase upload time
run: |
touch app.tar.gz
tar -czf app.tar.gz --exclude=app.tar.gz .
- name: Upload artifact for deployment job
uses: actions/upload-artifact@v2
with:
name: php-app
path: app.tar.gz
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: $
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v2
with:
name: php-app
- name: Extract Tar
run: |
tar -xf app.tar.gz
rm app.tar.gz
- name: 'Deploy to Azure Web App'
uses: azure/webapps-deploy@v2
id: deploy-to-webapp
with:
app-name: '{Your App Service Name}'
slot-name: 'Production'
publish-profile: ${ Your Publish Profile Secert Rerfence }
package: .
Adding Drupal Write / Presistantance
It is possible to mount Azure File Storage with App Cache Enabled. With this way your team can presist contents by only writing to your Azure File Share
Full Content Comming Soon.