Grav on Azure - High Level Architecture
Knowledge not shared is knowledge squandered.
Having made the decision to rebuild my site from the ground up in Grav, I went seeking documentation on how best to deploy it into Azure App Service. Such documentation did not exist, so I'm using the opportunity to hopefully make someone else's life easier further down the road.
I'll go through a typical design process here before moving into lower level design and deployment. I have access to quite a lot of free Azure credits, so I'm going to go pretty overboard on the design here vs what a typical blog requires. This will be useful for me in deploying and managing a production-grade infrastructure that I can tinker with as I please, as well as managing the software deployment lifecycle end to end.
My goals are as follows:
- Deploy Grav into Azure
- Make the site easily scalable
- Make it look pretty (to my eyes at least)
- Make the site geo-resilient
- Make the site extremely responsive, anywhere in the world
- Make blog updates easy on any device
- Make blog updates easy from Visual Studio Code in particular
- Secure the site
- Minimise cost
As is often the case with customer environments, prior to embarking on this journey I had no prior knowledge of the application itself. As I write this entry I am on day five of the journey now, and all of the above requirements have been met. The rest of this blog and the related series will recount the process I went through in that journey.
"Decisions are the hardest thing to make, especially when it is a choice between where you should be, and where you want to be."
There are myriad ways to deploy web services within Azure, including but not limited to...
- Virtual Machines
- Virtual Machine Scale Sets
- Windows Container in Virtual Machines
- Linux Container in Virtual Machines
- App Service on Windows
- App Service on Linux
- Containers in App Service on Linux
- Azure Container Instances
- Azure Kubernetes Service
...and that's just for the front end, never mind supporting and caching services. Fortunately with Grav there is no database back end to worry about. Interoperation between front and back end is often a critical design consideration, and the flat-file nature of Grav removes that decision point from this process :)
I know I don't want to manage virtual machines, I also want simple scalability, built in GitHub integration and a robust deployment pipeline, and no need to manage load balancers. With these criteria the choice is straightforward - the Azure App Service delivers on all these needs. I'm opting for App Service on Windows as my deployment vehicle here, though Linux would work just as well.
This choice doesn't preclude me from containerising the workload at a later date and moving to a Linux Container in the App Service if there's an advantage to doing so, I just need to make sure that any codebase changes or application decisions don't have Windows-specific dependencies.
Assuming I want to take advantage of scaling features within Azure App Service, right now this gives me an extremely simple architecture that looks pretty much like the below image.
I want to make this application resilient across regions though, and I want to be able to serve my two most common audiences with reduced latency, so I'm going to deploy a web app in each of two regions - one in West Europe, and one in East US.
In order to provide a single DNS entry for them which will respond based on requestor geography, I'll use Azure Traffic Manager. At this stage the how isn't important, we're just laying out the functional components to use. Later blog entries will delve into the practicalities of deployment and configuration.
With a second region and Traffic Manager in place, the overall architecture now looks like this.
Grav offers several out of the box integrations for for improving performance, including Memcache, Memcached, APC, APCu, Xcache, Wincache, or Redis. As I'm aiming for a VMless deployment making use of native Azure services wherever possible, integrating with Redis for caching purposes makes the most sense for me. Caching is very important for Grav performance, there's some further reading available here.
How to actually deploy Azure Redis is a different matter entirely, and there are some decisions to be made even at this high level architecture stage which will have performance, resiliency, and cost implications further down the line.
There are three tiers of Redis cache available in Azure, each with different features and performance profiles, and of course associated cost.
Under the hood, Azure Redis Basic just provides a single virtual machine and no SLA, which for my purposes isn't acceptable. Redis Standard offers a 99.9% SLA, running the service on a resilient master/slave setup. Redis Premium offers geo-replication, with one region offering read/write and a secondary offering read access to the cache.
The cost for Premium starts at a minimum of around £300 per month, which puts it outside the level I'm willing to spend.
To provide a cache in each region, I'm going to use Redis Standard C1, and deploy a separate instance into each region. Each Web App will therefore have its own cache, which will require slight config differences in each region to accommodate. At a high level, this then changes the architecture to look like the following:
The last element I want to incorporate into the high level architecture here is some form of CDN integration, so static content can be geo-distributed. I know that Grav can support integration with a CDN, so I'll add it into the plans just now, and worry about the how further down the line :)
As I say, this is massively overboard for a typical blog site - a single shared Web App with page caching enabled in IIS/Apache would work fine for most blogs for about £7 per month.
In the next entry I'll deploy the infrastructure components, and make some choices about code deployment pipeline.