From 7b378a7737e311fd8053d35ff4d99055e86de911 Mon Sep 17 00:00:00 2001 From: Catherine Wang Date: Wed, 20 Sep 2023 09:59:31 -0700 Subject: [PATCH 1/5] adding cache --- azureproject/production.py | 12 ++++++++++++ azureproject/settings.py | 10 ++++++++++ restaurant_review/views.py | 2 ++ 3 files changed, 24 insertions(+) diff --git a/azureproject/production.py b/azureproject/production.py index 1a95b3ec..a5c397c5 100644 --- a/azureproject/production.py +++ b/azureproject/production.py @@ -38,3 +38,15 @@ 'PASSWORD': conn_str_params['password'], } } + +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": os.environ.get('CACHELOCATION'), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor", + 'PASSWORD': os.environ.get('CACHEKEY') + }, + } + } \ No newline at end of file diff --git a/azureproject/settings.py b/azureproject/settings.py index 83e6d8d0..d8c08831 100644 --- a/azureproject/settings.py +++ b/azureproject/settings.py @@ -119,6 +119,16 @@ }, ] +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": os.environ.get('CACHELOCATION'), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + 'PASSWORD': os.environ.get('CACHEKEY') + }, + } + } # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ diff --git a/restaurant_review/views.py b/restaurant_review/views.py index bf02215e..110356a7 100644 --- a/restaurant_review/views.py +++ b/restaurant_review/views.py @@ -4,11 +4,13 @@ from django.urls import reverse from django.utils import timezone from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.cache import cache_page from restaurant_review.models import Restaurant, Review # Create your views here. +@cache_page(60) def index(request): print('Request for index page received') restaurants = Restaurant.objects.annotate(avg_rating=Avg('review__rating')).annotate(review_count=Count('review')) From 180192d6c2333ced9577c79dd5637ef970a72b63 Mon Sep 17 00:00:00 2001 From: Catherine Wang Date: Wed, 20 Sep 2023 11:15:15 -0700 Subject: [PATCH 2/5] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/main_restaurantreview.yml | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/main_restaurantreview.yml diff --git a/.github/workflows/main_restaurantreview.yml b/.github/workflows/main_restaurantreview.yml new file mode 100644 index 00000000..df870ef2 --- /dev/null +++ b/.github/workflows/main_restaurantreview.yml @@ -0,0 +1,63 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions +# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions + +name: Build and deploy Python app to Azure Web App - restaurantReview + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python version + uses: actions/setup-python@v1 + with: + python-version: '3.11' + + - name: Create and start virtual environment + run: | + python -m venv venv + source venv/bin/activate + + - name: Install dependencies + run: pip install -r requirements.txt + + # Optional: Add step to run tests here (PyTest, Django test suites, etc.) + + - name: Upload artifact for deployment jobs + uses: actions/upload-artifact@v2 + with: + name: python-app + path: | + . + !venv/ + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v2 + with: + name: python-app + path: . + + - name: 'Deploy to Azure Web App' + uses: azure/webapps-deploy@v2 + id: deploy-to-webapp + with: + app-name: 'restaurantReview' + slot-name: 'Production' + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_0EB1DB70D6D34D01BE7BA033F7890571 }} From 124b33932e73ea59ff8fa8d31f6949a9d0f4f8d5 Mon Sep 17 00:00:00 2001 From: Catherine Wang Date: Sun, 24 Sep 2023 11:22:00 -0700 Subject: [PATCH 3/5] azd for cache and private endpoint --- infra/resources.bicep | 111 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/infra/resources.bicep b/infra/resources.bicep index 3e3577ff..9efefad8 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -10,8 +10,16 @@ param secretKey string var prefix = '${name}-${resourceToken}' var pgServerName = '${prefix}-postgres-server' +//added for Redis Cache +var cacheServerName = '${prefix}-redisCache' var databaseSubnetName = 'database-subnet' var webappSubnetName = 'webapp-subnet' +//added for Redis Cache +var cacheSubnetName = 'cache-subnet' +//added for Redis Cache +var cachePrivateEndpointName = 'cache-privateEndpoint' +//added for Redis Cache +var cachePvtEndpointDnsGroupName = 'cacheDnsGroup' resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-11-01' = { name: '${prefix}-vnet' @@ -52,6 +60,12 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-11-01' = { ] } } + { + name: cacheSubnetName + properties:{ + addressPrefix: '10.0.2.0/24' + } + } ] } resource databaseSubnet 'subnets' existing = { @@ -60,6 +74,10 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-11-01' = { resource webappSubnet 'subnets' existing = { name: webappSubnetName } + //added for Redis Cache + resource cacheSubnet 'subnets' existing = { + name: cacheSubnetName + } } resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { @@ -71,6 +89,16 @@ resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { ] } +// added for Redis Cache +resource privateDnsZoneCache 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.redis.cache.windows.net' + location: 'global' + tags: tags + dependsOn:[ + virtualNetwork + ] +} + resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { parent: privateDnsZone name: '${pgServerName}-link' @@ -83,6 +111,54 @@ resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLin } } + //added for Redis Cache +resource privateDnsZoneLinkCache 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + parent: privateDnsZoneCache + name: 'privatelink.redis.cache.windows.net-applink' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: virtualNetwork.id + } + } +} + + +resource cachePrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = { + name: cachePrivateEndpointName + location: location + properties: { + subnet: { + id: virtualNetwork::cacheSubnet.id + } + privateLinkServiceConnections: [ + { + name: cachePrivateEndpointName + properties: { + privateLinkServiceId: redisCache.id + groupIds: [ + 'redisCache' + ] + } + } + ] + } + resource cachePvtEndpointDnsGroup 'privateDnsZoneGroups' = { + name: cachePvtEndpointDnsGroupName + properties: { + privateDnsZoneConfigs: [ + { + name: 'privatelink-redis-cache-windows-net' + properties: { + privateDnsZoneId: privateDnsZoneCache.id + } + } + ] + } + } +} + resource web 'Microsoft.Web/sites@2022-03-01' = { name: '${prefix}-app-service' location: location @@ -92,7 +168,7 @@ resource web 'Microsoft.Web/sites@2022-03-01' = { serverFarmId: appServicePlan.id siteConfig: { alwaysOn: true - linuxFxVersion: 'PYTHON|3.10' + linuxFxVersion: 'PYTHON|3.11' ftpsState: 'Disabled' appCommandLine: 'startup.sh' } @@ -108,6 +184,7 @@ resource web 'Microsoft.Web/sites@2022-03-01' = { AZURE_POSTGRESQL_CONNECTIONSTRING: 'dbname=${djangoDatabase.name} host=${postgresServer.name}.postgres.database.azure.com port=5432 sslmode=require user=${postgresServer.properties.administratorLogin} password=${databasePassword}' SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' SECRET_KEY: secretKey + //TODO: add settings for Redis Cache } } @@ -151,7 +228,7 @@ resource appServicePlan 'Microsoft.Web/serverfarms@2021-03-01' = { location: location tags: tags sku: { - name: 'B1' + name: 'S1' } properties: { reserved: true @@ -229,5 +306,35 @@ resource djangoDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@202 name: 'django' } +//added for Redis Cache +resource redisCache 'Microsoft.Cache/redis@2023-04-01' = { + location:location + name:cacheServerName + properties:{ + sku:{ + capacity: 1 + family:'C' + name:'Standard' + } + enableNonSslPort:false + redisVersion:'6' + publicNetworkAccess:'Disabled' + //subnetId:virtualNetwork::cacheSubnet.id //commented out b/c vnet injection only works for premium skus + } + + // resource redisCacheNetwork 'privateEndpointConnections' = { + // name: '${cacheServerName}-privateEndpointConnection' + // properties:{ + // privateLinkServiceConnectionState: { + // actionsRequired: 'Change on the service provider will require updates on the consumer. see https://learn.microsoft.com/en-us/azure/templates/microsoft.cache/redis/privateendpointconnections?pivots=deployment-language-bicep#privatelinkserviceconnectionstate' + // description: 'the private link service connection state for setting up private link' + // status: 'Approved' + // } + // privateEndpoint: cachePrivateEndpoint + // } + // } + +} + output WEB_URI string = 'https://${web.properties.defaultHostName}' output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsightsResources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING From a4e3b9938df18baf1f488114d23cda7a6f8470c9 Mon Sep 17 00:00:00 2001 From: Catherine Wang Date: Thu, 28 Sep 2023 11:52:46 -0700 Subject: [PATCH 4/5] adding cache to the environment. Adding cached output and session to the app --- azureproject/production.py | 1 + azureproject/settings.py | 1 + infra/resources.bicep | 2 ++ requirements.txt | 3 ++- restaurant_review/templates/restaurant_review/index.html | 3 +++ restaurant_review/views.py | 8 +++++--- 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/azureproject/production.py b/azureproject/production.py index a5c397c5..6de3455b 100644 --- a/azureproject/production.py +++ b/azureproject/production.py @@ -22,6 +22,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +SESSION_ENGINE = "django.contrib.sessions.backends.cache" STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') diff --git a/azureproject/settings.py b/azureproject/settings.py index d8c08831..89e42d15 100644 --- a/azureproject/settings.py +++ b/azureproject/settings.py @@ -53,6 +53,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +SESSION_ENGINE = "django.contrib.sessions.backends.cache" ROOT_URLCONF = 'azureproject.urls' TEMPLATES = [ diff --git a/infra/resources.bicep b/infra/resources.bicep index 9efefad8..0c8e19f3 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -185,6 +185,8 @@ resource web 'Microsoft.Web/sites@2022-03-01' = { SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' SECRET_KEY: secretKey //TODO: add settings for Redis Cache + CACHELOCATION: 'rediss://${redisCache.name}.redis.cache.windows.net:6380/0' + CACHEKEY: redisCache.listKeys().primaryKey } } diff --git a/requirements.txt b/requirements.txt index 1aa13135..1ad821c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Django==4.2.5 psycopg2-binary==2.9.7 python-dotenv==1.0.0 -whitenoise==6.5.0 \ No newline at end of file +whitenoise==6.5.0 +django-redis==5.3.0 \ No newline at end of file diff --git a/restaurant_review/templates/restaurant_review/index.html b/restaurant_review/templates/restaurant_review/index.html index 2abf02b2..0f637000 100644 --- a/restaurant_review/templates/restaurant_review/index.html +++ b/restaurant_review/templates/restaurant_review/index.html @@ -42,6 +42,9 @@ {% endblock %} {% block content %} + {% if LastViewedRestaurant %} +

Last viewed restaurant: {{ LastViewedRestaurant }}

+ {% endif %}

Restaurants

{% if restaurants %} diff --git a/restaurant_review/views.py b/restaurant_review/views.py index 110356a7..29120a0f 100644 --- a/restaurant_review/views.py +++ b/restaurant_review/views.py @@ -10,16 +10,18 @@ # Create your views here. -@cache_page(60) def index(request): print('Request for index page received') restaurants = Restaurant.objects.annotate(avg_rating=Avg('review__rating')).annotate(review_count=Count('review')) - return render(request, 'restaurant_review/index.html', {'restaurants': restaurants}) - + lastViewedRestaurant = request.session.get("lastViewedRestaurant", False) + print(lastViewedRestaurant) + return render(request, 'restaurant_review/index.html', {'LastViewedRestaurant': lastViewedRestaurant, 'restaurants': restaurants}) +@cache_page(60) def details(request, id): print('Request for restaurant details page received') restaurant = get_object_or_404(Restaurant, pk=id) + request.session["lastViewedRestaurant"] = restaurant.name return render(request, 'restaurant_review/details.html', {'restaurant': restaurant}) From 88e1d1022219d1b3d8443972e42d0372f5d44d3c Mon Sep 17 00:00:00 2001 From: Catherine Wang Date: Wed, 25 Oct 2023 11:20:21 -0700 Subject: [PATCH 5/5] adding info to the cache branch --- README.md | 7 +++++++ infra/resources.bicep | 12 ------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7f327836..99e3eb24 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ This is a Python web app using the Django framework and the Azure Database for PostgreSQL relational database service. The Django app is hosted in a fully managed Azure App Service. This app is designed to be be run locally and then deployed to Azure. You can either deploy this project by following the tutorial [*Deploy a Python (Django or Flask) web app with PostgreSQL in Azure*](https://docs.microsoft.com/azure/app-service/tutorial-python-postgresql-app) or by using the [Azure Developer CLI (azd)](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview) according to the instructions below. +## The Cache branch +The Cache branch in the repository contains sample code to save web response out and user session data in a Redis Cache. For web response caching, if a user clicks into the detailed view of a resturant, the details page will be cached for 60 seconds. This increases the application performance by saving server resource and reducing dependency calls into the PostgreSQL. For user session data such as last viewed restaurant, saving in Redis Cache improves consistency and resiliency comparing with in-memory. +Files changed in the Cache branch: +* ./azureproject/production.py and settings.py - added cache and session middlewares +* ./restaurant_review/views.py - added @cache_page tag and session data "lastViewedRestaurant" +* ./restaurant_review/templates/restaurant_review/index.html - added display for 'LastViewedRestaurant' + ## Requirements The [requirements.txt](./requirements.txt) has the following packages: diff --git a/infra/resources.bicep b/infra/resources.bicep index 0c8e19f3..ad24d717 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -321,20 +321,8 @@ resource redisCache 'Microsoft.Cache/redis@2023-04-01' = { enableNonSslPort:false redisVersion:'6' publicNetworkAccess:'Disabled' - //subnetId:virtualNetwork::cacheSubnet.id //commented out b/c vnet injection only works for premium skus } - // resource redisCacheNetwork 'privateEndpointConnections' = { - // name: '${cacheServerName}-privateEndpointConnection' - // properties:{ - // privateLinkServiceConnectionState: { - // actionsRequired: 'Change on the service provider will require updates on the consumer. see https://learn.microsoft.com/en-us/azure/templates/microsoft.cache/redis/privateendpointconnections?pivots=deployment-language-bicep#privatelinkserviceconnectionstate' - // description: 'the private link service connection state for setting up private link' - // status: 'Approved' - // } - // privateEndpoint: cachePrivateEndpoint - // } - // } }