.NET on Pivotal Cloud Foundry: Scaling an App on Diego

by Raman YurkinMay 4, 2016
This tutorial explains how to scale and modify a sample app, add a session state storage, test it on multiple instances, and push it to Pivotal CF.

net-on-pcf-scaling-diego-v1

Earlier in this blog series, we deployed a .NET app to Diego on Pivotal CF and bound it to a MS SQL service. However, hardly any cloud app exists as a single instance. So, here, we will scale the demo app horizontally and test how it works on multiple instances.

 

Checking application scale settings

First of all, let’s view the current scale settings with the cf scale command:

> cf scale SampleWebApp
Showing current scale of app SampleWebApp in org altoros / space dev as example@altoros.com...
OK
memory: 1G
disk: 1G
instances: 1
>

As you can see, we can configure the number of application instances and their size.

For testing purposes, we have added one more view and controller action to our demo app (the source code can be found in this GitHub repo). The action puts the number of current instances into the session variable, so we can find out if the session is shared by all instances or not.

After we have published the updated version of SampleWebApp into Pivotal CF, we can check the results.

net-on-pivotalcf-diego-scaling-session-test

Apparently, there is only one instance of the application.

 

Scaling the demo app

We’re going to increase the number of instances to three and view the result.

> cf scale SampleWebapp -i 3
Scaling app SampleWebApp in org altoros / space dev as altoros...
OK
> cf scale SampleWebapp
Showing current scale of app SampleWebApp in org altoros / space dev as altoros...
OK
memory: 1G
disk: 1G
instances: 3
>

If we refresh the browser a few times, we will see what is illustrated in the picture below.

net-on-pivotalcf-scaling-diego-session-tests

Each time, the application is working as a separate instance that knows nothing about the other ones. To handle this issue, we need to provide a common session and state storage.

 

Adding a session state storage

A session state can be saved in a memory cache solution or some database / custom session storage (e.g., Redis or Memcache). For this small project, we chose Redis, an open-source cache / storage, which is available as a service on Pivotal CF. To enable it, we will use Microsoft Redis Session State Provider.

To create a Redis service and bind it to the application:

  1. Check what services are available in the marketplace.
  2. > cf marketplace
    Getting services from marketplace in org altoros / space dev as example@altoros.com...
    OK
    service      plans  		       	   description
    p-redis      dedicated-vm, shared-vm       Redis service to provide a key-value store
    TIP:  Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.
    >
  3. Create a service instance from the marketplace.
  4. > cf create-service p-redis shared-vm sampleredisservice
    Creating service instance sampleredisservice in org altoros / space dev as example@altoros.com...
    OK
    >
  5. Bind the service instance to the application.
  6. > cf bind-service samplewebapp sampleredisservice
    Binding service sampleredisservice to app SampleWebApp in org altoros / space dev as example@altoros.com...
    OK
    TIP: Use 'cf.exe restage SampleWebApp' to ensure your env variable changes take effect
    >

The Pivotal CF part is ready.

 

Modifying the demo app

We still need to modify our .NET app. Let’s add Redis Session Store Provider by using the NuGet package manager.

PM> Install-Package Microsoft.Web.RedisSessionStateProvider

After that, we will enable the app to receive Redis connection strings from the application environment.

  1. In Web.config:
  2. <sessionstate mode="Custom" customProvider="MySessionStateStore">
      	<providers>
        	<add name="MySessionStateStore" type="Microsoft.Web.Redis.RedisSessionStateProvider" host="" accessKey="" ssl="true" settingsClassName="SampleWebApp.Startup" settingsMethodName="GetRedisConnectionString"></add>
      	</providers>
    </sessionstate>
  3. In Startup.cs:
  4. public partial class Startup
    	{
     
    …
     
     
        	public static string GetRedisConnectionString()
        	{
            	var settings = ParseVCAP();
           	 ConfigurationOptions config = new ConfigurationOptions
            	{
                	EndPoints =
                    	{
                        	{ settings.RedisHost, int.Parse(settings.RedisPort)}
                    	},
                	CommandMap = CommandMap.Create(new HashSet<string>
                	{ // EXCLUDE a few commands
                    	/*"INFO", "CONFIG", "CLUSTER",
                    	"PING", "ECHO", "CLIENT"*/
                	}, available: false),
                	KeepAlive = 180,
                	DefaultVersion = new Version(2, 8, 8),
                	Password = settings.RedisPassword
            	};
     
            	return config.ToString();
        	}
     
        	public static Credentials ParseVCAP()
        	{
            	Credentials result = new Credentials();
            	const string dbServiceName = "mssql-dev";
            	const string redisServiceName = "p-redis";
            	string vcapServices = Environment.GetEnvironmentVariable("VCAP_SERVICES");
     
            	// if we are in the cloud and DB service was bound successfully...
            	if (vcapServices != null)
            	{
                	dynamic json = JsonConvert.DeserializeObject(vcapServices);
                	foreach (dynamic obj in json.Children())
                	{
     
                        switch (((string)obj.Name).ToLowerInvariant())
                        {
                        case dbServiceName:
                              {
                              dynamic credentials = (((JProperty)obj).Value[0] as dynamic).credentials;
                              result.DBHost = credentials?.host;
                              result.DBPort = credentials?.port;
                              result.DBUID = credentials?.username;
                              result.DBPassword = credentials?.password;
                              result.DBConnectionString = credentials?.connectionString;
                              }
                              break;
     
                        case redisServiceName:
              	          {
                              dynamic credentials = (((JProperty)obj).Value[0] as dynamic).credentials;
                              result.RedisHost = credentials?.host;
                              result.RedisPort = credentials?.port;
                              result.RedisPassword = credentials?.password;
                              }
                              break;
     
                        default:
                              break;
                        }
     
               	 }
            	}
            	return result;
        	}
     
    	}
     
    	public struct Credentials
    	{
        	public string DBUID;
        	public string DBHost;
        	public string DBPort;
        	public string DBPassword;
        	public string DBConnectionString;
        	public string RedisHost;
        	public string RedisPort;
        	public string RedisPassword;
    	}</string></pre>
    </ol>
    
    <p>Now, we can read Redis settings from an environment variable and put the prepared connection string into the Redis session storage provider.</p>
    
    &nbsp;
    <h3>CF push!</h3>
    
    <p>Finally, we can publish the app to Pivotal CF and check the result.</p>
    
    <pre style="padding-left: 30px;">cf push SampleWebApp -p .\ -s windows2012R2 --no-start -b
    cf enable-diego SampleWebApp
    cf start SampleWebApp

    The output:

    net-on-pvotal-cf-scaling-diego-session-test

    As you have probably noticed, all the session data is shared between instances. This means the application is working well.

     

    Further reading

     


    This post was written by Raman Yurkin, edited by Sophia Turol and Alex Khizhniak.