DEV Community

Cover image for Optimizely on Kubernetes
Christopher Jansson
Christopher Jansson

Posted on

Optimizely on Kubernetes

Introduction

This article shows you how to set up the Optimizely CMS sample site using Kubernetes. The article also covers topics such as scaling and load balancing, and some gotchas found when installing.

The article is based on Google Cloud for hosting the database
and provisioning a Kubernetes
cluster, but you can of course use Azure or an on-premise
installation if it suits you better.

Pre-requisites

This assumes you has the following:

  • Kubernetes Cluster (In this article i use Google Kubernetes Engine)
  • Azure Servicebus (Standard or Premium tier)
  • Azure Storage Account
  • SQL Server (2012 or later)
  • Linux or WSL2

Getting the sample site

The sample site used in this article is found here.
Start by cloning the repository:

git clone git@github.com:episerver/netcore-preview.git
cd netcore-preview
Enter fullscreen mode Exit fullscreen mode

The netcore-preview repository contains 2 sample web sites: Quicksilver and Alloy. This article focus on running the Alloy web site. To speed up building the solution, remove the Quicksilver project by running the following command in the same directory as the solution:

dotnet sln remove  Quicksilver/EPiServer.Reference.Commerce.Site/EPiServer.Reference.Commerce.Site.csproj
Enter fullscreen mode Exit fullscreen mode

Creating the database

To create a new Optimizely database we first need to install the new EPiserver CLI using the following command:

dotnet tool install EPiServer.Net.Cli --global --add-source https://pkgs.dev.azure.com/EpiserverEngineering/netCore/_packaging/beta-program/nuget/v3/index.json --version 1.0.0-pre-020034
Enter fullscreen mode Exit fullscreen mode

When the CLI is done installing you can run the following command to create the Optimizely CMS database:

dotnet-episerver create-cms-database Alloy/AlloyMvcTemplates.csproj -S {SQL SERVER ADDRESS} -U {USER} -P {PASSWORD}

When the installer is done, the EPiServerDB connectionstring is appended to the appSettings.json
configuration file located in the root of the project.

To create an admin user run the following command:

dotnet-episerver add-admin-user Alloy/AlloyMvcTemplates.csproj -u username -p password -e user@email.com -c EPiServerDB

Configuring the website to run multiple instances

There are some things we must do in order to make the site able to run multiple instances

  • Configuring EventProvider
  • Configuring BlobStorage
  • Adding Dataprotection

Adding Azure EventProvider & BlobProvider

In a loadbalanced website events must be distributed to all instances, this is carried out by the EventProvider.
In this article I will use Azure Servicebus to distribute the events. Optimizely provides a implementation for replicating events, AzureEventProvider found in the EPiServer.Azure NuGet package.

However we cannot use AzureEventProvider directly when we run on top of Kubernetes since Servicebus SubscriptionNames has a limitation of 50 characters and AzureEventProvider sets SubscriptionName using the following code snippet: MachineName + Guid.NewGuid().ToString("N").

When running a Kubernetes Deployment, pod names get a hash suffix. This name is used as the machine name, so when using AzureEventProvider the SubscriptionName would be: podname + hash + 32 character guid which is to many characters due to the 50 character restriction in Azure Service Bus.

To circumvent this, we create a custom implementation which inherits AzureEventProvider and uses System.Reflection magic to set SubscriptionName. The implementation can be found here

Install the required NuGet package by running the following command:
dotnet add package EPiServer.Azure

When the NuGet package(s) has been installed you need to add the following code shown below:
Register services in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
  ...
        // register blob storage
        services.AddAzureBlobProvider(o => {
            o.ConnectionString = Environment.GetEnvironmentVariable("BLOBSTORAGE_CONNECTIONSTRING");
        });

        // register event provider
        services.AddEventProvider<CustomAzureEventProvider>();
        services.Configure<CustomAzureEventProviderOptions>(o => {
            o.ConnectionString = Environment.GetEnvironmentVariable("SERVICEBUS_CONNECTIONSTRING");
            o.TopicName = "siteevents";
            o.SubscriptionName =  Guid.NewGuid().ToString("N");
        });

  ...
}
Enter fullscreen mode Exit fullscreen mode

Configuring Dataprotection

In order to encrypt/decrypt and validate view-state, a shared key on each instance of our application needs to be used. This was previously done in ASP.NET Framework by setting the <machineKey> element under <system.web>. In ASP.NET Core, this has been replaced by DataProtection.

To distribute and persist our key to each instance we will use Azure Blobstorage.
Note that in production, you probably want to encrypt the keys at rest aswell. You can read more about this here

Install required NuGet by running the following command: dotnet add package Azure.Extensions.AspNetCore.DataProtection.Blobs

Add the code to Startup.cs

public void ConfigureServices(IServiceCollection services)
{
  ...
  services
    .AddDataProtection()
    .PersistKeysToAzureBlobStorage(Environment.GetEnvironmentVariable("BLOBSTORAGE_CONNECTIONSTRING"), "keys", "epidemo");

  ...
}
Enter fullscreen mode Exit fullscreen mode

Configuring License

Getting a developer license for Optimizely can be done here.
License

Instead of bundling container specfic configuration with our application, a better way is to mount configuration either as files in an attached volume or as environment variables.
By default, Optimizely checks for the Licence.config file in the app root directory. We need to configure the application to check for the license in another directory where the license will be mounted.
To configure this add the following to Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.Configure<Licensing.LicensingOptions>(o => {
        o.LicenseFilePath = "/etc/app/License.config";
    });

    ...
}
Enter fullscreen mode Exit fullscreen mode

Dockerizing the project

The final thing we need to do in order to run the site on Kubernetes is to dockerize the application.

Create a new Dockerfile in the root directory

FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build-env
WORKDIR /app

COPY NuGet.config .
COPY dependencies.props .
COPY ./Alloy/AlloyMvcTemplates.csproj   ./Alloy/
COPY *.sln .
RUN dotnet restore

COPY . .
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim
WORKDIR /app
COPY --from=build-env /app/out .

EXPOSE 80

ENTRYPOINT ["dotnet", "AlloyMvcTemplates.dll"]
Enter fullscreen mode Exit fullscreen mode

Deploying the site on Kubernetes

Since our docker image does not contain any ConnectionStrings or Licensefile we need to inject them into the container. This is done by using Kubernetes Secrets & ConfigMap.
By separating configuration and application the configuration could be reused acros multiple applications.

Creating Connection string Secret

Create a new secret containg our connectionstrings by running the following command:

kubectl create secret generic epi-connectionstring \ 
--from-literal=eventprovider='{EVENT_PROVIDER_CONNECTION_STRING}' \
--from-literal=blobstorage='{BLOB_STORAGE_CONNECTION_STRING}' \
--from-literal=database='{DB_CONNECTION_STRING}'
Enter fullscreen mode Exit fullscreen mode

Creating a License ConfigMap

Create a new configmap by running the following command:

kubectl create configmap epi-demo-license --from-file=License.config=PATH_TO_License.config 
Enter fullscreen mode Exit fullscreen mode

Creating Deployment Manifest

Deployments are used to run a specified number of replicas at any given time.

Create a new deployment by running the following command:

kubectl apply -f https://raw.githubusercontent.com/crikke/epi-ha-k8s-article/master/deployment.yml
Enter fullscreen mode Exit fullscreen mode

Run kubectl get deployments epi-k8s to verify that the deploy is created

NAME      READY   UP-TO-DATE   AVAILABLE   AGE
epi-k8s   2/2     2            2           3h11m
Enter fullscreen mode Exit fullscreen mode

Loadbalancing and making the application reachable outside the cluster

Currently, the application is only reachable within the cluster. In order to expose it to the internet, a Service is needed which routes incomming traffic to pods. Selecting which pod a service should route traffic to is done by using matching Pod labels.

Labels are key/value pairs that are attached to objects, such as pods. Labels are intended to be used to specify
identifying attributes of objects that are meaningful and relevant to users, but do not directly imply semantics to the
core system. Labels can be used to organize and to select subsets of objects.

Read more about labels at Kubernetes official documentation here

Create a new service by running the following command:

kubectl apply -f https://github.com/crikke/epi-ha-k8s-article/blob/master/service.yml
Enter fullscreen mode Exit fullscreen mode

Verify that the service is created by running kubectl get service epi-k8s-demo

You should get similar output as the example shown below:

NAME           TYPE           CLUSTER-IP   EXTERNAL-IP    PORT(S)                      AGE
epi-k8s-demo   LoadBalancer   10.44.7.49   34.88.54.254   80:30080/TCP,443:30443/TCP   155m
Enter fullscreen mode Exit fullscreen mode

And thats it, you now have Optimizely CMS running in a Kubernetes cluster!

Your application should now be reachable by the EXTERNAL-IP field from your service. Note that if it´s <PENDING>, wait some minutes and check again.

If you visit the site through EXTERNAL-IP, you could get an 404 error. This is because you first need to add a new site inside the Optimizely Admin UI. Visit EXTERNAL-IP/episerver/cms and go to Admin > Configuration > Manage Sites > Add Site

Conclusion

The Optimizely Devteam has done massive work of migrating Optimizely from .NET Framework and all underlying dependencies. Not only does this open many possibilities on hosting your application, but brings performance boosts aswell.

I hope this post helped you get some insight how to run Optimizely CMS on Kubernetes.
Happy coding!

Top comments (1)

Collapse
 
snorrejablonski profile image
Snorre J.

Hi, I am trying to set up the new Optimizely 12 Foundation project (github.com/episerver/foundation-mv...) in Kubernetes, but with limited experience with Epi/Optimizely, Docker, and Kubernetes I am really struggling to get the deployment working in Kubernetes inside my clients environment. I have managed to get the project working with this guide from Optimizely: world.optimizely.com/blogs/Mark-Ha...

Though with your links not being valid anymore (404) and not getting the kubectl create generic secret command working I am pretty stuck and do not even know where I should start searching to get my knowledge up to a level where I can get use of any of the guides out there. I would love to hear from you on your thoughts as from this article it seems that you are one of few with both experience with Optimizely and Kubernetes. Best regards, Snorre