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 }} 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/azureproject/production.py b/azureproject/production.py index 1a95b3ec..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') @@ -38,3 +39,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..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 = [ @@ -119,6 +120,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/infra/resources.bicep b/infra/resources.bicep index 3e3577ff..ad24d717 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,9 @@ 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 + CACHELOCATION: 'rediss://${redisCache.name}.redis.cache.windows.net:6380/0' + CACHEKEY: redisCache.listKeys().primaryKey } } @@ -151,7 +230,7 @@ resource appServicePlan 'Microsoft.Web/serverfarms@2021-03-01' = { location: location tags: tags sku: { - name: 'B1' + name: 'S1' } properties: { reserved: true @@ -229,5 +308,23 @@ 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' + } + + +} + output WEB_URI string = 'https://${web.properties.defaultHostName}' output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsightsResources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING 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 bf02215e..29120a0f 100644 --- a/restaurant_review/views.py +++ b/restaurant_review/views.py @@ -4,6 +4,7 @@ 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 @@ -12,12 +13,15 @@ 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})