With more and more organisations starting to implement devops automation to make use of cloud infrastructure and services, more consideration needs to be given to content management systems that rely heavily on a cache for performance in order to keep content in sync across a dynamic number of nodes. With the introduction of flexible load balancing in Umbraco 7.3+ this has become much easier, but can this be achieved in earlier versions?
Continuous Deployment Setup
In our scenario, we are creating a new auto scaling group inside AWS for each deployment we make using Octopus Deploy, which will create us a number of new instances based on a custom AMI we have already saved away. During the initialisation phase, we install Octopus Tentacles and have them "call home" to our Octopus Deployment server. This allows us to register the tentacles to the correct environments in our deployment server, so we can deploy the latest release and also reach the dynamic instances without having to know about them before hand. If we start to receive a large amount of load, then we can scale out and the same process occurs.
Implementation
The real challenge is ensuring that content is refreshed on all of these nodes when content is published using our admin server, which is a fixed instance. To achieve this we hook into the publish events using event handlers that sit inside the Umbraco website using some custom code and make use of the Octopus.Client NuGet package
namespace Umbraco { using System; using System.Configuration; using Octopus.Client; using Octopus.Client.Model; using global::Umbraco.Core; using global::Umbraco.Core.Events; using global::Umbraco.Core.Models; using global::Umbraco.Core.Publishing; using global::Umbraco.Core.Services; /// <summary> /// Register events to watch, so we can call back to Octopus Deploy and force a refresh on the nodes in the environment. /// </summary> public class DistributedContentRefreshEvents : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { // Listen for when content is being saved or published ContentService.Saved += this.ContentServiceOnSaved; ContentService.Published += this.ContentServiceOnPublished; } /// <summary> /// Called when a content saved event is raised. /// </summary> /// <param name="sender">The sender.</param> /// <param name="saveEventArgs">The <see cref="SaveEventArgs{IContent}"/> instance containing the event data.</param> private void ContentServiceOnSaved(IContentService sender, SaveEventArgs<IContent> saveEventArgs) { this.DistributeContentRefresh(); } /// <summary> /// Called when a content published event is raised. /// </summary> /// <param name="sender">The sender.</param> /// <param name="publishEventArgs">The <see cref="PublishEventArgs{IContent}"/> instance containing the event data.</param> private void ContentServiceOnPublished(IPublishingStrategy sender, PublishEventArgs<IContent> publishEventArgs) { this.DistributeContentRefresh(); } /// <summary> /// Distributes the content refresh by promoting a release in Octopus Deploy. /// </summary> private void DistributeContentRefresh() { try { var apiEndpoint = ConfigurationManager.AppSettings["OctopusApiEndpoint"]; var apiKey = ConfigurationManager.AppSettings["OctopusApiKey"]; var project = ConfigurationManager.AppSettings["OctopusRefreshContentProject"]; var environment = ConfigurationManager.AppSettings["OctopusRefreshContentEnvironment"]; var repository = new OctopusRepository(new OctopusServerEndpoint(apiEndpoint, apiKey)); // Get the project Id var projectId = repository.Projects.FindByName(project).Id; // Get the environment Id var environmentId = repository.Environments.FindByName(environment).Id; // Get latest release id var releaseId = repository.Releases.FindOne(x => x.ProjectId == projectId).Id; repository.Deployments.Create(new DeploymentResource { ReleaseId = releaseId, EnvironmentId = environmentId }); } catch (Exception) { } } } }
What we are doing here is essentially telling Octopus Deploy to promote a release for a specific project name in a specific environment, whenever a save or publish event is handled. This will call back to Octopus to push a "refresh content" deployment out to the tentacles in AWS - we have to worry about instances coming and going as Octopus Deploy is holding the information for us. The refresh content deployment invokes a page locally on the website that causes it to refresh the cache with the latest changes from the database.
This will call back to our default website on the instance and fire a simple line of code that's exposed as an API endpoint in Umbraco
namespace Umbraco.Api namespace Umbraco.Api { using System.Diagnostics.CodeAnalysis; using global::Umbraco.Web.BaseRest; [RestExtension("Cache")] public class CacheApi { /// <summary> /// Refreshes the content xml file to pick up new changes from the DB. /// </summary> /// <remarks>This is available via http://[ip]/base/cache/refreshcontent </remarks> [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] [RestExtensionMethod(ReturnXml = false)] public static void RefreshContent() { umbraco.library.RefreshContent(); } } }
If you're after devops automation engineering then why not Contact Us for a chat to see how we can help.