
On this page
A national tourism platform needed more than VM Scale Sets could offer. During campaigns, heavy traffic crashed the front end, and manual server tasks delayed releases. I led the shift to Azure PaaS, added Cloudflare and Azure Front Door, secured all data services, and set up each environment with a single Terraform module. Now, campaign traffic is handled automatically, and we release updates several times a week with approval steps. In one day, Cloudflare handled 99.2% of 55.8 million requests without hitting the origin.
This case study is for architects and engineers managing enterprise CMS platforms on Azure. The insights here are useful for Sitecore, Umbraco, Kentico, and Optimizely.
At a glance
Item | Detail |
|---|---|
Platform | Multiple public websites on Sitecore and .NET, with Optimizely experimentation and a Next.js front end |
My role | Cloud architecture, security, networking, CI/CD and production delivery |
Before | Sitecore on VM Scale Sets, React under PM2 |
After | Azure App Service, Container Apps, SQL Managed Instance, Redis, Key Vault, managed identities, Blob Storage, Service Bus |
Edge | Cloudflare, then Azure Front Door |
Network | One virtual network per environment, private endpoints, site-to-site VPN |
Delivery | Terraform and Azure DevOps across development, UAT and production |
Edge result | 99.2% of 55.8M requests served at the edge in 24 hours |
The starting point
The platform ran several public sites using Sitecore and .NET, sharing both identity and content. Editors published new content daily, and we released updates several times a week.
The platform had two main needs. Public sites needed to handle unpredictable traffic, while legacy on-premises and partner systems required private, secure connections because they couldn't be exposed to the internet.
Our first setup used VM Scale Sets. We managed the servers running Sitecore services, and React ran on those same VMs using PM2, a Node.js process manager. This worked initially, but as traffic increased, the platform became difficult to manage.
What broke
- The front end crashed during peak times. Tourism traffic surged during campaigns and holidays, overwhelming the PM2 processes and causing outages when demand was highest.
- The cache crashed too. Nginx failed under the same load. We cleared its cache by hand whenever it served stale content.
- Sitecore was unreliable at busy times. Content Management and Content Delivery were not consistently available. Editors struggled to publish campaign content, and Content Delivery instability reached the public site.
- Operations were mostly manual. We had to restart servers ourselves, and IIS application pool problems kept coming back. Troubleshooting required checking each server one by one.
- Environments became inconsistent. We set up development, UAT, and production manually, and each server stored its configuration in web.config files.
- Releases sometimes happened out of order. Code deployments and content publishing were separate, so sequence mistakes were common.
Why we moved to PaaS
The bottleneck had changed from compute to operations. Adding VMs would only add more servers to patch, restart, and keep consistent. So we asked a different question: which components should we keep managing ourselves? We handed the rest to Azure.
Area | Before | After |
|---|---|---|
Sitecore hosting | VM Scale Sets, IIS | Content Management, Content Delivery and Identity as separate App Services |
Front end | React under PM2 on VMs | React on App Service, Container Apps for containerised workloads |
Caching | Nginx, cleared by hand | Cloudflare caching plus incremental static regeneration, where React refreshes a page in the background |
Environments | Built by hand | Terraform |
Databases | SQL Server | Azure SQL Managed Instance, with geo-redundant backups |
Media | Blob Storage | Blob Storage, now reached only through a private endpoint |
Secrets | Stored on servers | Key Vault with managed identities |
Release and monitoring | Manual deploys, per-VM monitoring | Azure DevOps pipelines with approval gates; Azure Monitor, Application Insights and Log Analytics |
The architecture: seven layers

- Edge. Cloudflare owns DNS and Transport Layer Security (TLS). It caches static assets and pages, runs a web application firewall (WAF) and bot rules, and puts an access policy in front of the Sitecore authoring and admin paths.
- Ingress. Front Door routes by host and path and runs health probes. Front Door accepts traffic only from Cloudflare, and App Services accept traffic only from Front Door. An attacker who finds an origin hostname gets nothing. Application Gateway adds a regional entry point where needed.
- Experience. Next.js runs on App Service, with Container Apps for containerized sites. Pages use ISR where content allows. Cloudflare serves repeat requests, which keeps traffic peaks away from the origin.
- Content and services. Content Management, Content Delivery, and Identity run as separate App Services, with Solr for search and Redis for sessions. The .NET APIs and Optimizely components run alongside them, in containers where that made deployment simpler. API Management controls how those APIs are exposed. Content Delivery serves public traffic, so visitors never reach the authoring service.
- Data. SQL Managed Instance holds the Sitecore and application databases, with geo-redundant backups. Redis handles sessions and output cache. A Storage Account holds the media library. Key Vault holds secrets and certificates, which applications read through managed identities.
- Network: Each environment uses its own virtual network, with a subnet for each tier and network security groups in between. SQL, Redis, Storage, and Key Vault all use private endpoints, so nothing is exposed publicly. Outbound traffic goes through NAT, and private DNS keeps hostnames consistent.
- Delivery and operations: Azure DevOps handles builds and deployments. Terraform defines the infrastructure and Cloudflare settings. Azure Monitor, Application Insights, and Log Analytics track metrics, logs, traces, and alerts.
The edge in practice

Cloudflare Security analytics for one 24 hour window. Cloudflare served 55.35M requests. Only 363.11k reached the origin.
This is what the edge layer buys. On the VM platform every spike landed on PM2. Here, the origin sees well under one percent of the traffic, and the spike visible at the start of the window never reached Azure.
Security controls
Security is built into every layer rather than added at the end:
- Cloudflare runs the WAF and bot rules for all public traffic.
- Sitecore authoring sits behind an access policy. A password alone is never enough.
- No data service has a public endpoint, and no secret lives on a server.
- Legacy systems are reached over a site-to-site VPN, so no new internet-facing port was opened.
- Every infrastructure change goes through a pull request and a pipeline.
- Key Vault holds every certificate and alerts 60 days before expiry.
Five design decisions and their trade-offs
Decision | Why | Trade-off |
|---|---|---|
Move to PaaS instead of adding VMs | The bottleneck was operations, not compute | We stopped managing hosts and focused on network, security, and release design |
Keep Cloudflare in front of Front Door | The organisation already ran DNS, WAF and bot rules in Cloudflare and the security team knew it. Front Door still provides Azure-native routing, health probes and origin control | Two layers overlap on CDN, WAF and TLS. A strict traffic rule keeps them aligned |
Private endpoints for every data service | A leaked connection string is useless outside the network, and security reviews became much shorter | Private DNS zones must link to every network that resolves the names. Build agents need a private path, and laptops cannot connect without one |
Site-to-site VPN for legacy systems | Legacy systems could not touch the internet. An API gateway with an open port was faster but added exposure | Added VPN latency, which was never the bottleneck. Legacy owners changed nothing |
One Terraform module for every environment | Hand-built environments differed in small ways that caused production-only failures | We had to import existing resources and fix drift before the module worked |
Private endpoints in Terraform
You pay the private networking cost once, because Terraform makes it repeatable. This Storage private endpoint includes the DNS zone group that makes name resolution work:
1resource "azurerm_private_endpoint" "storage_blob" {2 name = "pe-storage-blob-${var.environment}"3 location = var.location4 resource_group_name = var.resource_group_name5 subnet_id = var.data_subnet_id67 private_service_connection {8 name = "psc-storage-blob"9 private_connection_resource_id = azurerm_storage_account.media.id10 subresource_names = ["blob"]11 is_manual_connection = false12 }1314 private_dns_zone_group {15 name = "blob-dns"16 private_dns_zone_ids = [azurerm_private_dns_zone.blob.id]17 }18}
Without the DNS zone group, applications keep resolving the public hostname and never reach the private address.
Adopting Terraform safely
Development, UAT and production use the same Terraform module with different variable files. Only resource sizes differ. We adopted it in this order:
- Start with non-production.
- Import what already exists.
- Fix drift in code.
- Touch production last.
Delivery and releases
Every application has its own pipeline with the same four stages:
- Build and test. Compile, run tests, build the package or container image.
- Deploy to development. Runs automatically after a successful build.
- Deploy to UAT. Runs after an approval.
- Deploy to production. Runs after an approval.
Terraform follows the same path. Each change starts as a pull request, the team reviews the plan there, and the pipeline applies it. Nobody applies from a laptop. Build agents run inside the virtual network so they can reach the private services they deploy to.
A production release follows six steps:
- Prove it. The change lands in development and UAT first.
- Review. For Terraform, the pull request plan is the record of what will change.
- Approve. The production stage waits for a manual approval.
- Deploy. The pipeline deploys the code, then runs the Sitecore publish stage. That order ends the old mix-ups between code and content.
- Verify. Smoke tests run, and Front Door health probes confirm the origins are live.
- Watch. Application Insights and alerts cover the first hours.
The aim is not risk-free releases. It is predictable, uneventful ones.
Monitoring
PaaS removes server work. It does not remove the need for monitoring. Application Insights runs on every App Service and traces requests across Next.js, the .NET APIs and Sitecore. Three conditions page someone:
- Availability tests failing from two regions
- Server error rate above threshold for five minutes
- A dependency such as SQL or Redis failing
Everything else goes to a dashboard. Each alert links to a runbook stored next to the Terraform. An alert says something is wrong. The runbook says what to do next.
Results
Cloudflare analytics for one 24 hour window ending 5 October 2026:
Metric | Value |
|---|---|
Total requests | 55.8M, about 650 per second on average |
Served by Cloudflare | 55.35M (99.2%) |
Reached the origin | 363.11k (0.65%) |
Mitigated, challenged or blocked | 84.85k (0.15%) |
- Cloudflare mitigated 84,850 requests in that window: 69,320 challenged and 15,530 blocked.
- Campaign traffic no longer depends on hand-managed PM2 processes.
- Nobody checks IIS application pools or clears Nginx caches by hand.
- Editors publish through a path that is tested after every release.
- All three environments come from one Terraform module.
- No data service has a public endpoint, and Sitecore authoring is never reachable from the open internet.
- Releases ship several times a week, with approvals before UAT and production.
- Nobody manages PM2 processes or patches application servers.
Lessons learned
- Scaling problems are not always compute problems. When process management, the operating system and scaling are tied to VMs, more machines add more to operate.
- Environment consistency matters as much as performance. Infrastructure as code is how you stop drift.
- Private networking touches everything. It changes DNS, build agents, developer access, testing and troubleshooting.
- Security controls need functional tests. A WAF can be on while the CMS is broken. A private endpoint can exist while DNS is wrong.
If you are planning a Sitecore, Umbraco, Kentico or Optimizely platform on Azure, the enterprise application and CMS deployments page on the main site describes how I run that work.
Sources
- Sitecore: Azure PaaS deployment and topology guidance
- Optimizely: hosting and deployment documentation
- Kentico Xperience: hosting on Azure
- Umbraco: hosting on Azure Web Apps
- Azure Front Door: secure your origin with Private Link and origin access control
- Azure Private Endpoint and private DNS integration
- Cloudflare WAF managed rules
- Terraform: azurerm_private_endpoint